diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-15 05:54:39 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-15 05:54:39 +0000 |
commit | 267c6f2ac71f92999e969232431ba04678e7437e (patch) | |
tree | 358c9467650e1d0a1d7227a21dac2e3d08b622b2 /basegfx/source | |
parent | Initial commit. (diff) | |
download | libreoffice-267c6f2ac71f92999e969232431ba04678e7437e.tar.xz libreoffice-267c6f2ac71f92999e969232431ba04678e7437e.zip |
Adding upstream version 4:24.2.0.upstream/4%24.2.0
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'basegfx/source')
56 files changed, 28438 insertions, 0 deletions
diff --git a/basegfx/source/color/bcolormodifier.cxx b/basegfx/source/color/bcolormodifier.cxx new file mode 100644 index 0000000000..75c06f5cb8 --- /dev/null +++ b/basegfx/source/color/bcolormodifier.cxx @@ -0,0 +1,539 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * 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 . + */ + +#include <sal/config.h> +#include <algorithm> +#include <float.h> +#include <basegfx/color/bcolormodifier.hxx> +#include <comphelper/random.hxx> + +namespace basegfx +{ + BColorModifier::~BColorModifier() + { + } + + BColorModifier_gray::~BColorModifier_gray() + { + } + + bool BColorModifier_gray::operator==(const BColorModifier& rCompare) const + { + return dynamic_cast< const BColorModifier_gray* >(&rCompare) != nullptr; + } + + ::basegfx::BColor BColorModifier_gray::getModifiedColor(const ::basegfx::BColor& aSourceColor) const + { + const double fLuminance(aSourceColor.luminance()); + + return ::basegfx::BColor(fLuminance, fLuminance, fLuminance); + } + + OUString BColorModifier_gray::getModifierName() const + { + return "gray"; + } + + BColorModifier_invert::~BColorModifier_invert() + { + } + + bool BColorModifier_invert::operator==(const BColorModifier& rCompare) const + { + return dynamic_cast< const BColorModifier_invert* >(&rCompare) != nullptr; + } + + ::basegfx::BColor BColorModifier_invert::getModifiedColor(const ::basegfx::BColor& aSourceColor) const + { + return ::basegfx::BColor(1.0 - aSourceColor.getRed(), 1.0 - aSourceColor.getGreen(), 1.0 - aSourceColor.getBlue()); + } + + OUString BColorModifier_invert::getModifierName() const + { + return "invert"; + } + + BColorModifier_luminance_to_alpha::~BColorModifier_luminance_to_alpha() + { + } + + bool BColorModifier_luminance_to_alpha::operator==(const BColorModifier& rCompare) const + { + return dynamic_cast< const BColorModifier_luminance_to_alpha* >(&rCompare) != nullptr; + } + + ::basegfx::BColor BColorModifier_luminance_to_alpha::getModifiedColor(const ::basegfx::BColor& aSourceColor) const + { + const double fAlpha(1.0 - ((aSourceColor.getRed() * 0.2125) + (aSourceColor.getGreen() * 0.7154) + (aSourceColor.getBlue() * 0.0721))); + + return ::basegfx::BColor(fAlpha, fAlpha, fAlpha); + } + + OUString BColorModifier_luminance_to_alpha::getModifierName() const + { + return "luminance_to_alpha"; + } + + BColorModifier_replace::~BColorModifier_replace() + { + } + + bool BColorModifier_replace::operator==(const BColorModifier& rCompare) const + { + const BColorModifier_replace* pCompare = dynamic_cast< const BColorModifier_replace* >(&rCompare); + + if(!pCompare) + { + return false; + } + + return getBColor() == pCompare->getBColor(); + } + + ::basegfx::BColor BColorModifier_replace::getModifiedColor(const ::basegfx::BColor& /*aSourceColor*/) const + { + return maBColor; + } + + OUString BColorModifier_replace::getModifierName() const + { + return "replace"; + } + + BColorModifier_interpolate::~BColorModifier_interpolate() + { + } + + bool BColorModifier_interpolate::operator==(const BColorModifier& rCompare) const + { + const BColorModifier_interpolate* pCompare = dynamic_cast< const BColorModifier_interpolate* >(&rCompare); + + if(!pCompare) + { + return false; + } + + return maBColor == pCompare->maBColor && mfValue == pCompare->mfValue; + } + + ::basegfx::BColor BColorModifier_interpolate::getModifiedColor(const ::basegfx::BColor& aSourceColor) const + { + return interpolate(maBColor, aSourceColor, mfValue); + } + + OUString BColorModifier_interpolate::getModifierName() const + { + return "interpolate"; + } + + BColorModifier_matrix::~BColorModifier_matrix() + { + } + + bool BColorModifier_matrix::operator==(const BColorModifier& rCompare) const + { + const BColorModifier_matrix* pCompare = dynamic_cast< const BColorModifier_matrix* >(&rCompare); + + if(!pCompare) + { + return false; + } + + return maVector == pCompare->maVector; + } + + ::basegfx::BColor BColorModifier_matrix::getModifiedColor(const ::basegfx::BColor& aSourceColor) const + { + if (maVector.size() != 20) + return aSourceColor; + + const double aRed = maVector[0] * aSourceColor.getRed() + + maVector[1] * aSourceColor.getGreen() + + maVector[2] * aSourceColor.getBlue() + + maVector[3] * 1.0 + + maVector[4]; + const double aGreen = maVector[5] * aSourceColor.getRed() + + maVector[6] * aSourceColor.getGreen() + + maVector[7] * aSourceColor.getBlue() + + maVector[8] * 1.0 + + maVector[9]; + const double aBlue = maVector[10] * aSourceColor.getRed() + + maVector[11] * aSourceColor.getGreen() + + maVector[12] * aSourceColor.getBlue() + + maVector[13] * 1.0 + + maVector[14]; + /*TODO: add support for alpha + const double aAlpha = maVector[15] * aSourceColor.getRed() + + maVector[16] * aSourceColor.getGreen() + + maVector[17] * aSourceColor.getBlue() + + maVector[18] * 1.0 + + maVector[19]); + */ + + return ::basegfx::BColor( + std::clamp(aRed, 0.0, 1.0), + std::clamp(aGreen, 0.0, 1.0), + std::clamp(aBlue, 0.0, 1.0)); + } + + OUString BColorModifier_matrix::getModifierName() const + { + return "matrix"; + } + + BColorModifier_saturate::BColorModifier_saturate(double fValue) + { + maSatMatrix.set(0, 0, 0.213 + 0.787 * fValue); + maSatMatrix.set(0, 1, 0.715 - 0.715 * fValue); + maSatMatrix.set(0, 2, 0.072 - 0.072 * fValue); + maSatMatrix.set(1, 0, 0.213 - 0.213 * fValue); + maSatMatrix.set(1, 1, 0.715 + 0.285 * fValue); + maSatMatrix.set(1, 2, 0.072 - 0.072 * fValue); + maSatMatrix.set(2, 0, 0.213 - 0.213 * fValue); + maSatMatrix.set(2, 1, 0.715 - 0.715 * fValue); + maSatMatrix.set(2, 2, 0.072 + 0.928 * fValue); + } + + BColorModifier_saturate::~BColorModifier_saturate() + { + } + + bool BColorModifier_saturate::operator==(const BColorModifier& rCompare) const + { + const BColorModifier_saturate* pCompare = dynamic_cast< const BColorModifier_saturate* >(&rCompare); + + if(!pCompare) + { + return false; + } + + return maSatMatrix == pCompare->maSatMatrix; + } + + ::basegfx::BColor BColorModifier_saturate::getModifiedColor(const ::basegfx::BColor& aSourceColor) const + { + basegfx::B3DHomMatrix aColorMatrix; + aColorMatrix.set(0, 0, aSourceColor.getRed()); + aColorMatrix.set(1, 0, aSourceColor.getGreen()); + aColorMatrix.set(2, 0, aSourceColor.getBlue()); + + aColorMatrix = maSatMatrix * aColorMatrix; + return ::basegfx::BColor(aColorMatrix.get(0, 0), aColorMatrix.get(1, 0), aColorMatrix.get(2, 0)); + } + + OUString BColorModifier_saturate::getModifierName() const + { + return "saturate"; + } + + BColorModifier_hueRotate::BColorModifier_hueRotate(double fRad) + { + const double fCos = cos(fRad); + const double fSin = sin(fRad); + + maHueMatrix.set(0, 0, 0.213 + fCos * 0.787 - fSin * 0.213); + maHueMatrix.set(0, 1, 0.715 - fCos * 0.715 - fSin * 0.715); + maHueMatrix.set(0, 2, 0.072 - fCos * 0.072 + fSin * 0.928); + maHueMatrix.set(1, 0, 0.213 - fCos * 0.213 + fSin * 0.143); + maHueMatrix.set(1, 1, 0.715 + fCos * 0.285 + fSin * 0.140); + maHueMatrix.set(1, 2, 0.072 - fCos * 0.072 - fSin * 0.283); + maHueMatrix.set(2, 0, 0.213 - fCos * 0.213 - fSin * 0.787); + maHueMatrix.set(2, 1, 0.715 - fCos * 0.715 + fSin * 0.715); + maHueMatrix.set(2, 2, 0.072 + fCos * 0.928 + fSin * 0.072); + } + + BColorModifier_hueRotate::~BColorModifier_hueRotate() + { + } + + bool BColorModifier_hueRotate::operator==(const BColorModifier& rCompare) const + { + const BColorModifier_hueRotate* pCompare = dynamic_cast< const BColorModifier_hueRotate* >(&rCompare); + + if(!pCompare) + { + return false; + } + + return maHueMatrix == pCompare->maHueMatrix; + } + + ::basegfx::BColor BColorModifier_hueRotate::getModifiedColor(const ::basegfx::BColor& aSourceColor) const + { + basegfx::B3DHomMatrix aColorMatrix; + aColorMatrix.set(0, 0, aSourceColor.getRed()); + aColorMatrix.set(1, 0, aSourceColor.getGreen()); + aColorMatrix.set(2, 0, aSourceColor.getBlue()); + + aColorMatrix = maHueMatrix * aColorMatrix; + return ::basegfx::BColor( + std::clamp(aColorMatrix.get(0, 0), 0.0, 1.0), + std::clamp(aColorMatrix.get(1, 0), 0.0, 1.0), + std::clamp(aColorMatrix.get(2, 0), 0.0, 1.0)); + } + + OUString BColorModifier_hueRotate::getModifierName() const + { + return "hueRotate"; + } + + BColorModifier_black_and_white::~BColorModifier_black_and_white() + { + } + + bool BColorModifier_black_and_white::operator==(const BColorModifier& rCompare) const + { + const BColorModifier_black_and_white* pCompare = dynamic_cast< const BColorModifier_black_and_white* >(&rCompare); + + if(!pCompare) + { + return false; + } + + return mfValue == pCompare->mfValue; + } + + ::basegfx::BColor BColorModifier_black_and_white::getModifiedColor(const ::basegfx::BColor& aSourceColor) const + { + const double fLuminance(aSourceColor.luminance()); + + if(fLuminance < mfValue) + { + return ::basegfx::BColor::getEmptyBColor(); + } + else + { + return ::basegfx::BColor(1.0, 1.0, 1.0); + } + } + + OUString BColorModifier_black_and_white::getModifierName() const + { + return "black_and_white"; + } + + BColorModifier_gamma::BColorModifier_gamma(double fValue) + : mfValue(fValue), + mfInvValue(fValue), + mbUseIt(!basegfx::fTools::equal(fValue, 1.0) && basegfx::fTools::more(fValue, 0.0) && basegfx::fTools::lessOrEqual(fValue, 10.0)) + { + if(mbUseIt) + { + mfInvValue = 1.0 / mfValue; + } + } + + BColorModifier_gamma::~BColorModifier_gamma() + { + } + + bool BColorModifier_gamma::operator==(const BColorModifier& rCompare) const + { + const BColorModifier_gamma* pCompare = dynamic_cast< const BColorModifier_gamma* >(&rCompare); + + if(!pCompare) + { + return false; + } + + // getValue is sufficient, mfInvValue and mbUseIt are only helper values + return mfValue == pCompare->mfValue; + } + + ::basegfx::BColor BColorModifier_gamma::getModifiedColor(const ::basegfx::BColor& aSourceColor) const + { + if(mbUseIt) + { + ::basegfx::BColor aRetval( + pow(aSourceColor.getRed(), mfInvValue), + pow(aSourceColor.getGreen(), mfInvValue), + pow(aSourceColor.getBlue(), mfInvValue)); + + aRetval.clamp(); + return aRetval; + } + else + { + return aSourceColor; + } + } + + OUString BColorModifier_gamma::getModifierName() const + { + return "gamma"; + } + + BColorModifier_RGBLuminanceContrast::BColorModifier_RGBLuminanceContrast(double fRed, double fGreen, double fBlue, double fLuminance, double fContrast) + : mfRed(std::clamp(fRed, -1.0, 1.0)), + mfGreen(std::clamp(fGreen, -1.0, 1.0)), + mfBlue(std::clamp(fBlue, -1.0, 1.0)), + mfLuminance(std::clamp(fLuminance, -1.0, 1.0)), + mfContrast(std::clamp(fContrast, -1.0, 1.0)), + mfContrastOff(1.0), + mfRedOff(0.0), + mfGreenOff(0.0), + mfBlueOff(0.0), + mbUseIt(false) + { + if(basegfx::fTools::equalZero(mfRed) + && basegfx::fTools::equalZero(mfGreen) + && basegfx::fTools::equalZero(mfBlue) + && basegfx::fTools::equalZero(mfLuminance) + && basegfx::fTools::equalZero(mfContrast)) + return; + + // calculate slope + if(mfContrast >= 0.0) + { + mfContrastOff = 128.0 / (128.0 - (mfContrast * 127.0)); + } + else + { + mfContrastOff = ( 128.0 + (mfContrast * 127.0)) / 128.0; + } + + // calculate unified contrast offset + const double fPreparedContrastOff((128.0 - mfContrastOff * 128.0) / 255.0); + const double fCombinedOffset(mfLuminance + fPreparedContrastOff); + + // set full offsets + mfRedOff = mfRed + fCombinedOffset; + mfGreenOff = mfGreen + fCombinedOffset; + mfBlueOff = mfBlue + fCombinedOffset; + + mbUseIt = true; + } + + BColorModifier_RGBLuminanceContrast::~BColorModifier_RGBLuminanceContrast() + { + } + + bool BColorModifier_RGBLuminanceContrast::operator==(const BColorModifier& rCompare) const + { + const BColorModifier_RGBLuminanceContrast* pCompare = dynamic_cast< const BColorModifier_RGBLuminanceContrast* >(&rCompare); + + if(!pCompare) + { + return false; + } + + // no need to compare other values, these are just helpers + return mfRed == pCompare->mfRed + && mfGreen == pCompare->mfGreen + && mfBlue == pCompare->mfBlue + && mfLuminance == pCompare->mfLuminance + && mfContrast == pCompare->mfContrast; + } + + ::basegfx::BColor BColorModifier_RGBLuminanceContrast::getModifiedColor(const ::basegfx::BColor& aSourceColor) const + { + if(mbUseIt) + { + return basegfx::BColor( + std::clamp(aSourceColor.getRed() * mfContrastOff + mfRedOff, 0.0, 1.0), + std::clamp(aSourceColor.getGreen() * mfContrastOff + mfGreenOff, 0.0, 1.0), + std::clamp(aSourceColor.getBlue() * mfContrastOff + mfBlueOff, 0.0, 1.0)); + } + else + { + return aSourceColor; + } + } + + OUString BColorModifier_RGBLuminanceContrast::getModifierName() const + { + return "RGBLuminanceContrast"; + } + + BColorModifier_randomize::BColorModifier_randomize(double fRandomPart) + : mfRandomPart(fRandomPart) + { + } + + BColorModifier_randomize::~BColorModifier_randomize() + { + } + + // compare operator + bool BColorModifier_randomize::operator==(const BColorModifier& rCompare) const + { + const BColorModifier_randomize* pCompare = dynamic_cast< const BColorModifier_randomize* >(&rCompare); + + if(!pCompare) + { + return false; + } + + return mfRandomPart == pCompare->mfRandomPart; + } + + // compute modified color + ::basegfx::BColor BColorModifier_randomize::getModifiedColor(const ::basegfx::BColor& aSourceColor) const + { + if(0.0 >= mfRandomPart) + { + // no randomizing, use orig color + return aSourceColor; + } + + if(1.0 <= mfRandomPart) + { + // full randomized color + return basegfx::BColor( + comphelper::rng::uniform_real_distribution(0.0, nextafter(1.0, DBL_MAX)), + comphelper::rng::uniform_real_distribution(0.0, nextafter(1.0, DBL_MAX)), + comphelper::rng::uniform_real_distribution(0.0, nextafter(1.0, DBL_MAX))); + } + + // mixed color + const double fMulA(1.0 - mfRandomPart); + return basegfx::BColor( + aSourceColor.getRed() * fMulA + + comphelper::rng::uniform_real_distribution(0.0, nextafter(mfRandomPart, DBL_MAX)), + aSourceColor.getGreen() * fMulA + + comphelper::rng::uniform_real_distribution(0.0, nextafter(mfRandomPart, DBL_MAX)), + aSourceColor.getBlue() * fMulA + + comphelper::rng::uniform_real_distribution(0.0, nextafter(mfRandomPart, DBL_MAX))); + } + + OUString BColorModifier_randomize::getModifierName() const + { + return "randomize"; + } + + ::basegfx::BColor BColorModifierStack::getModifiedColor(const ::basegfx::BColor& rSource) const + { + if(maBColorModifiers.empty()) + { + return rSource; + } + + ::basegfx::BColor aRetval(rSource); + + for(sal_uInt32 a(maBColorModifiers.size()); a;) + { + a--; + aRetval = maBColorModifiers[a]->getModifiedColor(aRetval); + } + + return aRetval; + } +} // end of namespace basegfx + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/basegfx/source/color/bcolortools.cxx b/basegfx/source/color/bcolortools.cxx new file mode 100644 index 0000000000..160b483289 --- /dev/null +++ b/basegfx/source/color/bcolortools.cxx @@ -0,0 +1,195 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * 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 . + */ + +#include <basegfx/numeric/ftools.hxx> +#include <basegfx/color/bcolor.hxx> +#include <basegfx/color/bcolortools.hxx> +#include <rtl/math.hxx> + +namespace basegfx::utils +{ + BColor rgb2hsl(const BColor& rRGBColor) + { + const double r=rRGBColor.getRed(), g=rRGBColor.getGreen(), b=rRGBColor.getBlue(); + const double minVal = std::min( std::min( r, g ), b ); + const double maxVal = std::max( std::max( r, g ), b ); + const double d = maxVal - minVal; + + double h=0, s=0, l=0; + + l = (maxVal + minVal) / 2.0; + + if( ::basegfx::fTools::equalZero(d) ) + { + s = h = 0; // hue undefined (achromatic case) + } + else + { + s = l > 0.5 ? d/(2.0-maxVal-minVal) : + d/(maxVal + minVal); + + if( rtl::math::approxEqual(r, maxVal) ) + h = (g - b)/d; + else if( rtl::math::approxEqual(g, maxVal) ) + h = 2.0 + (b - r)/d; + else + h = 4.0 + (r - g)/d; + + h *= 60.0; + + if( h < 0.0 ) + h += 360.0; + } + + return BColor(h,s,l); + } + + static double hsl2rgbHelper( double nValue1, double nValue2, double nHue ) + { + // clamp hue to [0,360] + nHue = fmod( nHue, 360.0 ); + + // cope with wrap-arounds + if( nHue < 0.0 ) + nHue += 360.0; + + if( nHue < 60.0 ) + return nValue1 + (nValue2 - nValue1)*nHue/60.0; + else if( nHue < 180.0 ) + return nValue2; + else if( nHue < 240.0 ) + return nValue1 + (nValue2 - nValue1)*(240.0 - nHue)/60.0; + else + return nValue1; + } + + BColor hsl2rgb(const BColor& rHSLColor) + { + const double h=rHSLColor.getRed(), s=rHSLColor.getGreen(), l=rHSLColor.getBlue(); + + if( fTools::equalZero(s) ) + return BColor(l, l, l ); // achromatic case + + const double nVal1( l <= 0.5 ? l*(1.0 + s) : l + s - l*s ); + const double nVal2( 2.0*l - nVal1 ); + + return BColor( + hsl2rgbHelper(nVal2, + nVal1, + h + 120.0), + hsl2rgbHelper(nVal2, + nVal1, + h), + hsl2rgbHelper(nVal2, + nVal1, + h - 120.0) ); + } + + BColor rgb2hsv(const BColor& rRGBColor) + { + const double r=rRGBColor.getRed(), g=rRGBColor.getGreen(), b=rRGBColor.getBlue(); + const double maxVal = std::max(std::max(r,g),b); + const double minVal = std::min(std::min(r,g),b); + const double delta = maxVal-minVal; + + double h=0, s=0, v=0; + + v = maxVal; + if( fTools::equalZero(v) ) + s = 0; + else + s = delta / v; + + if( !fTools::equalZero(s) ) + { + if( rtl::math::approxEqual(maxVal, r) ) + { + h = (g - b) / delta; + } + else if( rtl::math::approxEqual(maxVal, g) ) + { + h = 2.0 + (b - r) / delta; + } + else + { + h = 4.0 + (r - g) / delta; + } + + h *= 60.0; + + if( h < 0 ) + h += 360; + } + + return BColor(h,s,v); + } + + BColor hsv2rgb(const BColor& rHSVColor) + { + double h=rHSVColor.getRed(); + const double s=rHSVColor.getGreen(), v=rHSVColor.getBlue(); + + if( fTools::equalZero(s) ) + { + // achromatic case: no hue. + return BColor(v,v,v); + } + else + { + if( fTools::equal(h, 360.0) ) + h = 0; // 360 degrees is equivalent to 0 degrees + + h /= 60.0; + const sal_Int32 intval = static_cast< sal_Int32 >( h ); + const double f = h - intval; + const double p = v*(1.0-s); + const double q = v*(1.0-(s*f)); + const double t = v*(1.0-(s*(1.0-f))); + + /* which hue area? */ + switch( intval ) + { + case 0: + return BColor(v,t,p); + + case 1: + return BColor(q,v,p); + + case 2: + return BColor(p,v,t); + + case 3: + return BColor(p,q,v); + + case 4: + return BColor(t,p,v); + + case 5: + return BColor(v,p,q); + + default: + // hue overflow + return BColor(); + } + } + } + +} // end of namespace + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/basegfx/source/curve/b2dbeziertools.cxx b/basegfx/source/curve/b2dbeziertools.cxx new file mode 100644 index 0000000000..8fd4c4b84c --- /dev/null +++ b/basegfx/source/curve/b2dbeziertools.cxx @@ -0,0 +1,117 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * 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 . + */ + +#include <basegfx/curve/b2dbeziertools.hxx> +#include <basegfx/curve/b2dcubicbezier.hxx> +#include <algorithm> + +namespace basegfx +{ + B2DCubicBezierHelper::B2DCubicBezierHelper(const B2DCubicBezier& rBase, sal_uInt32 nDivisions) + : mnEdgeCount(0) + { + const bool bIsBezier(rBase.isBezier()); + + if(bIsBezier) + { + // check nDivisions; at least one is needed, but also prevent too big values + if(nDivisions < 1) + { + nDivisions = 1; + } + else if(nDivisions > 1000) + { + nDivisions = 1000; + } + + // set nEdgeCount + mnEdgeCount = nDivisions + 1; + + // fill in maLengthArray + maLengthArray.clear(); + maLengthArray.reserve(mnEdgeCount); + B2DPoint aCurrent(rBase.getStartPoint()); + double fLength(0.0); + + for(sal_uInt32 a(1);;) + { + const B2DPoint aNext(rBase.interpolatePoint(static_cast<double>(a) / static_cast<double>(mnEdgeCount))); + const B2DVector aEdge(aNext - aCurrent); + + fLength += aEdge.getLength(); + maLengthArray.push_back(fLength); + + if(++a < mnEdgeCount) + { + aCurrent = aNext; + } + else + { + const B2DPoint& aLastNext(rBase.getEndPoint()); + const B2DVector aLastEdge(aLastNext - aNext); + + fLength += aLastEdge.getLength(); + maLengthArray.push_back(fLength); + break; + } + } + } + else + { + maLengthArray.clear(); + maLengthArray.push_back(rBase.getEdgeLength()); + mnEdgeCount = 1; + } + } + + double B2DCubicBezierHelper::distanceToRelative(double fDistance) const + { + if(fDistance <= 0.0) + { + return 0.0; + } + + const double fLength(getLength()); + + if(fTools::moreOrEqual(fDistance, fLength)) + { + return 1.0; + } + + // fDistance is in ]0.0 .. fLength[ + + if(mnEdgeCount == 1) + { + // not a bezier, linear edge + return fDistance / fLength; + } + + // it is a bezier + std::vector< double >::const_iterator aIter = std::lower_bound(maLengthArray.begin(), maLengthArray.end(), fDistance); + const sal_uInt32 nIndex(aIter - maLengthArray.begin()); + const double fHighBound(maLengthArray[nIndex]); + const double fLowBound(nIndex ? maLengthArray[nIndex - 1] : 0.0); + const double fLinearInterpolatedLength((fDistance - fLowBound) / (fHighBound - fLowBound)); + + return (static_cast< double >(nIndex) + fLinearInterpolatedLength) / static_cast< double >(mnEdgeCount); + } + +} // end of namespace basegfx + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/basegfx/source/curve/b2dcubicbezier.cxx b/basegfx/source/curve/b2dcubicbezier.cxx new file mode 100644 index 0000000000..d33cd82b19 --- /dev/null +++ b/basegfx/source/curve/b2dcubicbezier.cxx @@ -0,0 +1,1055 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * 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 . + */ + +#include <basegfx/curve/b2dcubicbezier.hxx> +#include <basegfx/vector/b2dvector.hxx> +#include <basegfx/polygon/b2dpolygon.hxx> +#include <basegfx/matrix/b2dhommatrix.hxx> +#include <basegfx/numeric/ftools.hxx> + +#include <osl/diagnose.h> + +#include <limits> + +// #i37443# +#define FACTOR_FOR_UNSHARPEN (1.6) +#ifdef DBG_UTIL +const double fMultFactUnsharpen = FACTOR_FOR_UNSHARPEN; +#endif + +namespace basegfx +{ + namespace + { + void ImpSubDivAngle( + const B2DPoint& rfPA, // start point + const B2DPoint& rfEA, // edge on A + const B2DPoint& rfEB, // edge on B + const B2DPoint& rfPB, // end point + B2DPolygon& rTarget, // target polygon + double fAngleBound, // angle bound in [0.0 .. 2PI] + bool bAllowUnsharpen, // #i37443# allow the criteria to get unsharp in recursions + sal_uInt16 nMaxRecursionDepth) // endless loop protection + { + if(nMaxRecursionDepth) + { + // do angle test + B2DVector aLeft(rfEA - rfPA); + B2DVector aRight(rfEB - rfPB); + + // #i72104# + if(aLeft.equalZero()) + { + aLeft = rfEB - rfPA; + } + + if(aRight.equalZero()) + { + aRight = rfEA - rfPB; + } + + const double fCurrentAngle(aLeft.angle(aRight)); + + if(fabs(fCurrentAngle) > (M_PI - fAngleBound)) + { + // end recursion + nMaxRecursionDepth = 0; + } + else + { + if(bAllowUnsharpen) + { + // #i37443# unsharpen criteria +#ifdef DBG_UTIL + fAngleBound *= fMultFactUnsharpen; +#else + fAngleBound *= FACTOR_FOR_UNSHARPEN; +#endif + } + } + } + + if(nMaxRecursionDepth) + { + // divide at 0.5 + const B2DPoint aS1L(average(rfPA, rfEA)); + const B2DPoint aS1C(average(rfEA, rfEB)); + const B2DPoint aS1R(average(rfEB, rfPB)); + const B2DPoint aS2L(average(aS1L, aS1C)); + const B2DPoint aS2R(average(aS1C, aS1R)); + const B2DPoint aS3C(average(aS2L, aS2R)); + + // left recursion + ImpSubDivAngle(rfPA, aS1L, aS2L, aS3C, rTarget, fAngleBound, bAllowUnsharpen, nMaxRecursionDepth - 1); + + // right recursion + ImpSubDivAngle(aS3C, aS2R, aS1R, rfPB, rTarget, fAngleBound, bAllowUnsharpen, nMaxRecursionDepth - 1); + } + else + { + rTarget.append(rfPB); + } + } + + void ImpSubDivAngleStart( + const B2DPoint& rfPA, // start point + const B2DPoint& rfEA, // edge on A + const B2DPoint& rfEB, // edge on B + const B2DPoint& rfPB, // end point + B2DPolygon& rTarget, // target polygon + const double& rfAngleBound) // angle bound in [0.0 .. 2PI] + { + sal_uInt16 nMaxRecursionDepth(8); + const B2DVector aLeft(rfEA - rfPA); + const B2DVector aRight(rfEB - rfPB); + bool bLeftEqualZero(aLeft.equalZero()); + bool bRightEqualZero(aRight.equalZero()); + bool bAllParallel(false); + + if(bLeftEqualZero && bRightEqualZero) + { + nMaxRecursionDepth = 0; + } + else + { + const B2DVector aBase(rfPB - rfPA); + const bool bBaseEqualZero(aBase.equalZero()); // #i72104# + + if(!bBaseEqualZero) + { + const bool bLeftParallel(bLeftEqualZero || areParallel(aLeft, aBase)); + const bool bRightParallel(bRightEqualZero || areParallel(aRight, aBase)); + + if(bLeftParallel && bRightParallel) + { + bAllParallel = true; + + if(!bLeftEqualZero) + { + double fFactor; + + if(fabs(aBase.getX()) > fabs(aBase.getY())) + { + fFactor = aLeft.getX() / aBase.getX(); + } + else + { + fFactor = aLeft.getY() / aBase.getY(); + } + + if(fFactor >= 0.0 && fFactor <= 1.0) + { + bLeftEqualZero = true; + } + } + + if(!bRightEqualZero) + { + double fFactor; + + if(fabs(aBase.getX()) > fabs(aBase.getY())) + { + fFactor = aRight.getX() / -aBase.getX(); + } + else + { + fFactor = aRight.getY() / -aBase.getY(); + } + + if(fFactor >= 0.0 && fFactor <= 1.0) + { + bRightEqualZero = true; + } + } + + if(bLeftEqualZero && bRightEqualZero) + { + nMaxRecursionDepth = 0; + } + } + } + } + + if(nMaxRecursionDepth) + { + // divide at 0.5 ad test both edges for angle criteria + const B2DPoint aS1L(average(rfPA, rfEA)); + const B2DPoint aS1C(average(rfEA, rfEB)); + const B2DPoint aS1R(average(rfEB, rfPB)); + const B2DPoint aS2L(average(aS1L, aS1C)); + const B2DPoint aS2R(average(aS1C, aS1R)); + const B2DPoint aS3C(average(aS2L, aS2R)); + + // test left + bool bAngleIsSmallerLeft(bAllParallel && bLeftEqualZero); + if(!bAngleIsSmallerLeft) + { + const B2DVector aLeftLeft(bLeftEqualZero ? aS2L - aS1L : aS1L - rfPA); // #i72104# + const B2DVector aRightLeft(aS2L - aS3C); + const double fCurrentAngleLeft(aLeftLeft.angle(aRightLeft)); + bAngleIsSmallerLeft = (fabs(fCurrentAngleLeft) > (M_PI - rfAngleBound)); + } + + // test right + bool bAngleIsSmallerRight(bAllParallel && bRightEqualZero); + if(!bAngleIsSmallerRight) + { + const B2DVector aLeftRight(aS2R - aS3C); + const B2DVector aRightRight(bRightEqualZero ? aS2R - aS1R : aS1R - rfPB); // #i72104# + const double fCurrentAngleRight(aLeftRight.angle(aRightRight)); + bAngleIsSmallerRight = (fabs(fCurrentAngleRight) > (M_PI - rfAngleBound)); + } + + if(bAngleIsSmallerLeft && bAngleIsSmallerRight) + { + // no recursion necessary at all + nMaxRecursionDepth = 0; + } + else + { + // left + if(bAngleIsSmallerLeft) + { + rTarget.append(aS3C); + } + else + { + ImpSubDivAngle(rfPA, aS1L, aS2L, aS3C, rTarget, rfAngleBound, true/*bAllowUnsharpen*/, nMaxRecursionDepth); + } + + // right + if(bAngleIsSmallerRight) + { + rTarget.append(rfPB); + } + else + { + ImpSubDivAngle(aS3C, aS2R, aS1R, rfPB, rTarget, rfAngleBound, true/*bAllowUnsharpen*/, nMaxRecursionDepth); + } + } + } + + if(!nMaxRecursionDepth) + { + rTarget.append(rfPB); + } + } + + void ImpSubDivDistance( + const B2DPoint& rfPA, // start point + const B2DPoint& rfEA, // edge on A + const B2DPoint& rfEB, // edge on B + const B2DPoint& rfPB, // end point + B2DPolygon& rTarget, // target polygon + double fDistanceBound2, // quadratic distance criteria + double fLastDistanceError2, // the last quadratic distance error + sal_uInt16 nMaxRecursionDepth) // endless loop protection + { + if(nMaxRecursionDepth) + { + // decide if another recursion is needed. If not, set + // nMaxRecursionDepth to zero + + // Perform bezier flatness test (lecture notes from R. Schaback, + // Mathematics of Computer-Aided Design, Uni Goettingen, 2000) + + // ||P(t) - L(t)|| <= max ||b_j - b_0 - j/n(b_n - b_0)|| + // 0<=j<=n + + // What is calculated here is an upper bound to the distance from + // a line through b_0 and b_3 (rfPA and P4 in our notation) and the + // curve. We can drop 0 and n from the running indices, since the + // argument of max becomes zero for those cases. + const double fJ1x(rfEA.getX() - rfPA.getX() - 1.0/3.0*(rfPB.getX() - rfPA.getX())); + const double fJ1y(rfEA.getY() - rfPA.getY() - 1.0/3.0*(rfPB.getY() - rfPA.getY())); + const double fJ2x(rfEB.getX() - rfPA.getX() - 2.0/3.0*(rfPB.getX() - rfPA.getX())); + const double fJ2y(rfEB.getY() - rfPA.getY() - 2.0/3.0*(rfPB.getY() - rfPA.getY())); + const double fDistanceError2(std::max(fJ1x*fJ1x + fJ1y*fJ1y, fJ2x*fJ2x + fJ2y*fJ2y)); + + // stop if error measure does not improve anymore. This is a + // safety guard against floating point inaccuracies. + // stop if distance from line is guaranteed to be bounded by d + const bool bFurtherDivision(fLastDistanceError2 > fDistanceError2 && fDistanceError2 >= fDistanceBound2); + + if(bFurtherDivision) + { + // remember last error value + fLastDistanceError2 = fDistanceError2; + } + else + { + // stop recursion + nMaxRecursionDepth = 0; + } + } + + if(nMaxRecursionDepth) + { + // divide at 0.5 + const B2DPoint aS1L(average(rfPA, rfEA)); + const B2DPoint aS1C(average(rfEA, rfEB)); + const B2DPoint aS1R(average(rfEB, rfPB)); + const B2DPoint aS2L(average(aS1L, aS1C)); + const B2DPoint aS2R(average(aS1C, aS1R)); + const B2DPoint aS3C(average(aS2L, aS2R)); + + // left recursion + ImpSubDivDistance(rfPA, aS1L, aS2L, aS3C, rTarget, fDistanceBound2, fLastDistanceError2, nMaxRecursionDepth - 1); + + // right recursion + ImpSubDivDistance(aS3C, aS2R, aS1R, rfPB, rTarget, fDistanceBound2, fLastDistanceError2, nMaxRecursionDepth - 1); + } + else + { + rTarget.append(rfPB); + } + } + } // end of anonymous namespace +} // end of namespace basegfx + +namespace basegfx +{ + B2DCubicBezier::B2DCubicBezier(const B2DCubicBezier&) = default; + + B2DCubicBezier::B2DCubicBezier() = default; + + B2DCubicBezier::B2DCubicBezier(const B2DPoint& rStart, const B2DPoint& rControlPointA, const B2DPoint& rControlPointB, const B2DPoint& rEnd) + : maStartPoint(rStart), + maEndPoint(rEnd), + maControlPointA(rControlPointA), + maControlPointB(rControlPointB) + { + } + + // assignment operator + B2DCubicBezier& B2DCubicBezier::operator=(const B2DCubicBezier&) = default; + + // compare operators + bool B2DCubicBezier::operator==(const B2DCubicBezier& rBezier) const + { + return ( + maStartPoint == rBezier.maStartPoint + && maEndPoint == rBezier.maEndPoint + && maControlPointA == rBezier.maControlPointA + && maControlPointB == rBezier.maControlPointB + ); + } + + bool B2DCubicBezier::operator!=(const B2DCubicBezier& rBezier) const + { + return ( + maStartPoint != rBezier.maStartPoint + || maEndPoint != rBezier.maEndPoint + || maControlPointA != rBezier.maControlPointA + || maControlPointB != rBezier.maControlPointB + ); + } + + bool B2DCubicBezier::equal(const B2DCubicBezier& rBezier) const + { + return ( + maStartPoint.equal(rBezier.maStartPoint) + && maEndPoint.equal(rBezier.maEndPoint) + && maControlPointA.equal(rBezier.maControlPointA) + && maControlPointB.equal(rBezier.maControlPointB) + ); + } + + // test if vectors are used + bool B2DCubicBezier::isBezier() const + { + return maControlPointA != maStartPoint || maControlPointB != maEndPoint; + } + + void B2DCubicBezier::testAndSolveTrivialBezier() + { + if(maControlPointA == maStartPoint && maControlPointB == maEndPoint) + return; + + const B2DVector aEdge(maEndPoint - maStartPoint); + + // controls parallel to edge can be trivial. No edge -> not parallel -> control can + // still not be trivial (e.g. ballon loop) + if(aEdge.equalZero()) + return; + + // get control vectors + const B2DVector aVecA(maControlPointA - maStartPoint); + const B2DVector aVecB(maControlPointB - maEndPoint); + + // check if trivial per se + bool bAIsTrivial(aVecA.equalZero()); + bool bBIsTrivial(aVecB.equalZero()); + + // #i102241# prepare inverse edge length to normalize cross values; + // else the small compare value used in fTools::equalZero + // will be length dependent and this detection will work as less + // precise as longer the edge is. In principle, the length of the control + // vector would need to be used too, but to be trivial it is assumed to + // be of roughly equal length to the edge, so edge length can be used + // for both. Only needed when one of both is not trivial per se. + const double fInverseEdgeLength(bAIsTrivial && bBIsTrivial + ? 1.0 + : 1.0 / aEdge.getLength()); + + // if A is not zero, check if it could be + if(!bAIsTrivial) + { + // #i102241# parallel to edge? Check aVecA, aEdge. Use cross() which does what + // we need here with the precision we need + const double fCross(aVecA.cross(aEdge) * fInverseEdgeLength); + + if(fTools::equalZero(fCross)) + { + // get scale to edge. Use bigger distance for numeric quality + const double fScale(fabs(aEdge.getX()) > fabs(aEdge.getY()) + ? aVecA.getX() / aEdge.getX() + : aVecA.getY() / aEdge.getY()); + + // relative end point of vector in edge range? + if (fTools::betweenOrEqualEither(fScale, 0.0, 1.0)) + { + bAIsTrivial = true; + } + } + } + + // if B is not zero, check if it could be, but only if A is already trivial; + // else solve to trivial will not be possible for whole edge + if(bAIsTrivial && !bBIsTrivial) + { + // parallel to edge? Check aVecB, aEdge + const double fCross(aVecB.cross(aEdge) * fInverseEdgeLength); + + if(fTools::equalZero(fCross)) + { + // get scale to edge. Use bigger distance for numeric quality + const double fScale(fabs(aEdge.getX()) > fabs(aEdge.getY()) + ? aVecB.getX() / aEdge.getX() + : aVecB.getY() / aEdge.getY()); + + // end point of vector in edge range? Caution: controlB is directed AGAINST edge + if (fTools::betweenOrEqualEither(fScale, -1.0, 0.0)) + { + bBIsTrivial = true; + } + } + } + + // if both are/can be reduced, do it. + // Not possible if only one is/can be reduced (!) + if(bAIsTrivial && bBIsTrivial) + { + maControlPointA = maStartPoint; + maControlPointB = maEndPoint; + } + } + + namespace { + double impGetLength(const B2DCubicBezier& rEdge, double fDeviation, sal_uInt32 nRecursionWatch) + { + const double fEdgeLength(rEdge.getEdgeLength()); + const double fControlPolygonLength(rEdge.getControlPolygonLength()); + const double fCurrentDeviation(fTools::equalZero(fControlPolygonLength) ? 0.0 : 1.0 - (fEdgeLength / fControlPolygonLength)); + + if(!nRecursionWatch || fTools:: lessOrEqual(fCurrentDeviation, fDeviation)) + { + return (fEdgeLength + fControlPolygonLength) * 0.5; + } + else + { + B2DCubicBezier aLeft, aRight; + const double fNewDeviation(fDeviation * 0.5); + const sal_uInt32 nNewRecursionWatch(nRecursionWatch - 1); + + rEdge.split(0.5, &aLeft, &aRight); + + return impGetLength(aLeft, fNewDeviation, nNewRecursionWatch) + + impGetLength(aRight, fNewDeviation, nNewRecursionWatch); + } + } + } + + double B2DCubicBezier::getLength(double fDeviation) const + { + if(isBezier()) + { + if(fDeviation < 0.00000001) + { + fDeviation = 0.00000001; + } + + return impGetLength(*this, fDeviation, 6); + } + else + { + return B2DVector(getEndPoint() - getStartPoint()).getLength(); + } + } + + double B2DCubicBezier::getEdgeLength() const + { + const B2DVector aEdge(maEndPoint - maStartPoint); + return aEdge.getLength(); + } + + double B2DCubicBezier::getControlPolygonLength() const + { + const B2DVector aVectorA(maControlPointA - maStartPoint); + const B2DVector aVectorB(maEndPoint - maControlPointB); + + if(!aVectorA.equalZero() || !aVectorB.equalZero()) + { + const B2DVector aTop(maControlPointB - maControlPointA); + return (aVectorA.getLength() + aVectorB.getLength() + aTop.getLength()); + } + else + { + return getEdgeLength(); + } + } + + void B2DCubicBezier::adaptiveSubdivideByAngle(B2DPolygon& rTarget, double fAngleBound) const + { + if(isBezier()) + { + // use support method #i37443# and allow unsharpen the criteria + ImpSubDivAngleStart(maStartPoint, maControlPointA, maControlPointB, maEndPoint, rTarget, + deg2rad(fAngleBound)); + } + else + { + rTarget.append(getEndPoint()); + } + } + + B2DVector B2DCubicBezier::getTangent(double t) const + { + if(fTools::lessOrEqual(t, 0.0)) + { + // tangent in start point + B2DVector aTangent(getControlPointA() - getStartPoint()); + + if(!aTangent.equalZero()) + { + return aTangent; + } + + // start point and control vector are the same, fallback + // to implicit start vector to control point B + aTangent = (getControlPointB() - getStartPoint()) * 0.3; + + if(!aTangent.equalZero()) + { + return aTangent; + } + + // not a bezier at all, return edge vector + return (getEndPoint() - getStartPoint()) * 0.3; + } + else if(fTools::moreOrEqual(t, 1.0)) + { + // tangent in end point + B2DVector aTangent(getEndPoint() - getControlPointB()); + + if(!aTangent.equalZero()) + { + return aTangent; + } + + // end point and control vector are the same, fallback + // to implicit start vector from control point A + aTangent = (getEndPoint() - getControlPointA()) * 0.3; + + if(!aTangent.equalZero()) + { + return aTangent; + } + + // not a bezier at all, return edge vector + return (getEndPoint() - getStartPoint()) * 0.3; + } + else + { + // t is in ]0.0 .. 1.0[. Split and extract + B2DCubicBezier aRight; + split(t, nullptr, &aRight); + + return aRight.getControlPointA() - aRight.getStartPoint(); + } + } + + // #i37443# adaptive subdivide by nCount subdivisions + void B2DCubicBezier::adaptiveSubdivideByCount(B2DPolygon& rTarget, sal_uInt32 nCount) const + { + const double fLenFact(1.0 / static_cast< double >(nCount + 1)); + + for(sal_uInt32 a(1); a <= nCount; a++) + { + const double fPos(static_cast< double >(a) * fLenFact); + rTarget.append(interpolatePoint(fPos)); + } + + rTarget.append(getEndPoint()); + } + + // adaptive subdivide by distance + void B2DCubicBezier::adaptiveSubdivideByDistance(B2DPolygon& rTarget, double fDistanceBound, int nRecurseLimit) const + { + if(isBezier()) + { + ImpSubDivDistance(maStartPoint, maControlPointA, maControlPointB, maEndPoint, rTarget, + fDistanceBound * fDistanceBound, std::numeric_limits<double>::max(), nRecurseLimit); + } + else + { + rTarget.append(getEndPoint()); + } + } + + B2DPoint B2DCubicBezier::interpolatePoint(double t) const + { + OSL_ENSURE(t >= 0.0 && t <= 1.0, "B2DCubicBezier::interpolatePoint: Access out of range (!)"); + + if(isBezier()) + { + const B2DPoint aS1L(interpolate(maStartPoint, maControlPointA, t)); + const B2DPoint aS1C(interpolate(maControlPointA, maControlPointB, t)); + const B2DPoint aS1R(interpolate(maControlPointB, maEndPoint, t)); + const B2DPoint aS2L(interpolate(aS1L, aS1C, t)); + const B2DPoint aS2R(interpolate(aS1C, aS1R, t)); + + return interpolate(aS2L, aS2R, t); + } + else + { + return interpolate(maStartPoint, maEndPoint, t); + } + } + + double B2DCubicBezier::getSmallestDistancePointToBezierSegment(const B2DPoint& rTestPoint, double& rCut) const + { + const sal_uInt32 nInitialDivisions(3); + B2DPolygon aInitialPolygon; + + // as start make a fix division, creates nInitialDivisions + 2 points + aInitialPolygon.append(getStartPoint()); + adaptiveSubdivideByCount(aInitialPolygon, nInitialDivisions); + + // now look for the closest point + const sal_uInt32 nPointCount(aInitialPolygon.count()); + B2DVector aVector(rTestPoint - aInitialPolygon.getB2DPoint(0)); + double fQuadDist(aVector.getX() * aVector.getX() + aVector.getY() * aVector.getY()); + double fNewQuadDist; + sal_uInt32 nSmallestIndex(0); + + for(sal_uInt32 a(1); a < nPointCount; a++) + { + aVector = B2DVector(rTestPoint - aInitialPolygon.getB2DPoint(a)); + fNewQuadDist = aVector.getX() * aVector.getX() + aVector.getY() * aVector.getY(); + + if(fNewQuadDist < fQuadDist) + { + fQuadDist = fNewQuadDist; + nSmallestIndex = a; + } + } + + // look right and left for even smaller distances + double fStepValue(1.0 / static_cast<double>((nPointCount - 1) * 2)); // half the edge step width + double fPosition(static_cast<double>(nSmallestIndex) / static_cast<double>(nPointCount - 1)); + + while(true) + { + // test left + double fPosLeft(fPosition - fStepValue); + + if(fPosLeft < 0.0) + { + fPosLeft = 0.0; + aVector = B2DVector(rTestPoint - maStartPoint); + } + else + { + aVector = B2DVector(rTestPoint - interpolatePoint(fPosLeft)); + } + + fNewQuadDist = aVector.getX() * aVector.getX() + aVector.getY() * aVector.getY(); + + if(fTools::less(fNewQuadDist, fQuadDist)) + { + fQuadDist = fNewQuadDist; + fPosition = fPosLeft; + } + else + { + // test right + double fPosRight(fPosition + fStepValue); + + if(fPosRight > 1.0) + { + fPosRight = 1.0; + aVector = B2DVector(rTestPoint - maEndPoint); + } + else + { + aVector = B2DVector(rTestPoint - interpolatePoint(fPosRight)); + } + + fNewQuadDist = aVector.getX() * aVector.getX() + aVector.getY() * aVector.getY(); + + if(fTools::less(fNewQuadDist, fQuadDist)) + { + fQuadDist = fNewQuadDist; + fPosition = fPosRight; + } + else + { + // not less left or right, done + break; + } + } + + if(fPosition == 0.0 || fPosition == 1.0) + { + // if we are completely left or right, we are done + break; + } + + // prepare next step value + fStepValue /= 2.0; + } + + rCut = fPosition; + return sqrt(fQuadDist); + } + + void B2DCubicBezier::split(double t, B2DCubicBezier* pBezierA, B2DCubicBezier* pBezierB) const + { + OSL_ENSURE(t >= 0.0 && t <= 1.0, "B2DCubicBezier::split: Access out of range (!)"); + + if(!pBezierA && !pBezierB) + { + return; + } + + if(isBezier()) + { + const B2DPoint aS1L(interpolate(maStartPoint, maControlPointA, t)); + const B2DPoint aS1C(interpolate(maControlPointA, maControlPointB, t)); + const B2DPoint aS1R(interpolate(maControlPointB, maEndPoint, t)); + const B2DPoint aS2L(interpolate(aS1L, aS1C, t)); + const B2DPoint aS2R(interpolate(aS1C, aS1R, t)); + const B2DPoint aS3C(interpolate(aS2L, aS2R, t)); + + if(pBezierA) + { + pBezierA->setStartPoint(maStartPoint); + pBezierA->setEndPoint(aS3C); + pBezierA->setControlPointA(aS1L); + pBezierA->setControlPointB(aS2L); + } + + if(pBezierB) + { + pBezierB->setStartPoint(aS3C); + pBezierB->setEndPoint(maEndPoint); + pBezierB->setControlPointA(aS2R); + pBezierB->setControlPointB(aS1R); + } + } + else + { + const B2DPoint aSplit(interpolate(maStartPoint, maEndPoint, t)); + + if(pBezierA) + { + pBezierA->setStartPoint(maStartPoint); + pBezierA->setEndPoint(aSplit); + pBezierA->setControlPointA(maStartPoint); + pBezierA->setControlPointB(aSplit); + } + + if(pBezierB) + { + pBezierB->setStartPoint(aSplit); + pBezierB->setEndPoint(maEndPoint); + pBezierB->setControlPointA(aSplit); + pBezierB->setControlPointB(maEndPoint); + } + } + } + + B2DCubicBezier B2DCubicBezier::snippet(double fStart, double fEnd) const + { + B2DCubicBezier aRetval; + + if(fTools::more(fStart, 1.0)) + { + fStart = 1.0; + } + else if(fTools::less(fStart, 0.0)) + { + fStart = 0.0; + } + + if(fTools::more(fEnd, 1.0)) + { + fEnd = 1.0; + } + else if(fTools::less(fEnd, 0.0)) + { + fEnd = 0.0; + } + + if(fEnd <= fStart) + { + // empty or NULL, create single point at center + const double fSplit((fEnd + fStart) * 0.5); + const B2DPoint aPoint(interpolate(getStartPoint(), getEndPoint(), fSplit)); + aRetval.setStartPoint(aPoint); + aRetval.setEndPoint(aPoint); + aRetval.setControlPointA(aPoint); + aRetval.setControlPointB(aPoint); + } + else + { + if(isBezier()) + { + // copy bezier; cut off right, then cut off left. Do not forget to + // adapt cut value when both cuts happen + const bool bEndIsOne(fTools::equal(fEnd, 1.0)); + const bool bStartIsZero(fTools::equalZero(fStart)); + aRetval = *this; + + if(!bEndIsOne) + { + aRetval.split(fEnd, &aRetval, nullptr); + + if(!bStartIsZero) + { + fStart /= fEnd; + } + } + + if(!bStartIsZero) + { + aRetval.split(fStart, nullptr, &aRetval); + } + } + else + { + // no bezier, create simple edge + const B2DPoint aPointA(interpolate(getStartPoint(), getEndPoint(), fStart)); + const B2DPoint aPointB(interpolate(getStartPoint(), getEndPoint(), fEnd)); + aRetval.setStartPoint(aPointA); + aRetval.setEndPoint(aPointB); + aRetval.setControlPointA(aPointA); + aRetval.setControlPointB(aPointB); + } + } + + return aRetval; + } + + B2DRange B2DCubicBezier::getRange() const + { + B2DRange aRetval(maStartPoint, maEndPoint); + + aRetval.expand(maControlPointA); + aRetval.expand(maControlPointB); + + return aRetval; + } + + bool B2DCubicBezier::getMinimumExtremumPosition(double& rfResult) const + { + std::vector< double > aAllResults; + + aAllResults.reserve(4); + getAllExtremumPositions(aAllResults); + + const sal_uInt32 nCount(aAllResults.size()); + + if(!nCount) + { + return false; + } + else if(nCount == 1) + { + rfResult = aAllResults[0]; + return true; + } + else + { + rfResult = *(std::min_element(aAllResults.begin(), aAllResults.end())); + return true; + } + } + + namespace + { + void impCheckExtremumResult(double fCandidate, std::vector< double >& rResult) + { + // check for range ]0.0 .. 1.0[ with excluding 1.0 and 0.0 clearly + // by using the equalZero test, NOT ::more or ::less which will use the + // ApproxEqual() which is too exact here + if(fCandidate > 0.0 && !fTools::equalZero(fCandidate)) + { + if(fCandidate < 1.0 && !fTools::equalZero(fCandidate - 1.0)) + { + rResult.push_back(fCandidate); + } + } + } + } + + void B2DCubicBezier::getAllExtremumPositions(std::vector< double >& rResults) const + { + rResults.clear(); + + // calculate the x-extrema parameters by zeroing first x-derivative + // of the cubic bezier's parametric formula, which results in a + // quadratic equation: dBezier/dt = t*t*fAX - 2*t*fBX + fCX + const B2DPoint aControlDiff( maControlPointA - maControlPointB ); + double fCX = maControlPointA.getX() - maStartPoint.getX(); + const double fBX = fCX + aControlDiff.getX(); + const double fAX = 3 * aControlDiff.getX() + (maEndPoint.getX() - maStartPoint.getX()); + + if(fTools::equalZero(fCX)) + { + // detect fCX equal zero and truncate to real zero value in that case + fCX = 0.0; + } + + if( !fTools::equalZero(fAX) ) + { + // derivative is polynomial of order 2 => use binomial formula + const double fD = fBX*fBX - fAX*fCX; + if( fD >= 0.0 ) + { + const double fS = sqrt(fD); + // calculate both roots (avoiding a numerically unstable subtraction) + const double fQ = fBX + ((fBX >= 0) ? +fS : -fS); + impCheckExtremumResult(fQ / fAX, rResults); + if( !fTools::equalZero(fS) ) // ignore root multiplicity + impCheckExtremumResult(fCX / fQ, rResults); + } + } + else if( !fTools::equalZero(fBX) ) + { + // derivative is polynomial of order 1 => one extrema + impCheckExtremumResult(fCX / (2 * fBX), rResults); + } + + // calculate the y-extrema parameters by zeroing first y-derivative + double fCY = maControlPointA.getY() - maStartPoint.getY(); + const double fBY = fCY + aControlDiff.getY(); + const double fAY = 3 * aControlDiff.getY() + (maEndPoint.getY() - maStartPoint.getY()); + + if(fTools::equalZero(fCY)) + { + // detect fCY equal zero and truncate to real zero value in that case + fCY = 0.0; + } + + if( !fTools::equalZero(fAY) ) + { + // derivative is polynomial of order 2 => use binomial formula + const double fD = fBY*fBY - fAY*fCY; + if( fD >= 0.0 ) + { + const double fS = sqrt(fD); + // calculate both roots (avoiding a numerically unstable subtraction) + const double fQ = fBY + ((fBY >= 0) ? +fS : -fS); + impCheckExtremumResult(fQ / fAY, rResults); + if( !fTools::equalZero(fS) ) // ignore root multiplicity + impCheckExtremumResult(fCY / fQ, rResults); + } + } + else if( !fTools::equalZero(fBY) ) + { + // derivative is polynomial of order 1 => one extrema + impCheckExtremumResult(fCY / (2 * fBY), rResults); + } + } + + void B2DCubicBezier::transform(const basegfx::B2DHomMatrix& rMatrix) + { + if(rMatrix.isIdentity()) + return; + + if(maControlPointA == maStartPoint) + { + maControlPointA = maStartPoint = rMatrix * maStartPoint; + } + else + { + maStartPoint *= rMatrix; + maControlPointA *= rMatrix; + } + + if(maControlPointB == maEndPoint) + { + maControlPointB = maEndPoint = rMatrix * maEndPoint; + } + else + { + maEndPoint *= rMatrix; + maControlPointB *= rMatrix; + } + } + + void B2DCubicBezier::fround() + { + if(maControlPointA == maStartPoint) + { + maControlPointA = maStartPoint = basegfx::B2DPoint( + basegfx::fround(maStartPoint.getX()), + basegfx::fround(maStartPoint.getY())); + } + else + { + maStartPoint = basegfx::B2DPoint( + basegfx::fround(maStartPoint.getX()), + basegfx::fround(maStartPoint.getY())); + maControlPointA = basegfx::B2DPoint( + basegfx::fround(maControlPointA.getX()), + basegfx::fround(maControlPointA.getY())); + } + + if(maControlPointB == maEndPoint) + { + maControlPointB = maEndPoint = basegfx::B2DPoint( + basegfx::fround(maEndPoint.getX()), + basegfx::fround(maEndPoint.getY())); + } + else + { + maEndPoint = basegfx::B2DPoint( + basegfx::fround(maEndPoint.getX()), + basegfx::fround(maEndPoint.getY())); + maControlPointB = basegfx::B2DPoint( + basegfx::fround(maControlPointB.getX()), + basegfx::fround(maControlPointB.getY())); + } + } +} // end of namespace basegfx + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/basegfx/source/inc/stringconversiontools.hxx b/basegfx/source/inc/stringconversiontools.hxx new file mode 100644 index 0000000000..797697d203 --- /dev/null +++ b/basegfx/source/inc/stringconversiontools.hxx @@ -0,0 +1,64 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * 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 . + */ + +#pragma once + +#include <sal/config.h> + +#include <string_view> + +#include <sal/types.h> +#include <rtl/ustring.hxx> + +namespace basegfx::internal + { + void skipSpaces(sal_Int32& io_rPos, + std::u16string_view rStr, + const sal_Int32 nLen); + + inline bool isOnNumberChar(const sal_Unicode aChar, + bool bSignAllowed) + { + const bool bPredicate( (u'0' <= aChar && u'9' >= aChar) + || (bSignAllowed && u'+' == aChar) + || (bSignAllowed && u'-' == aChar) + || (u'.' == aChar)); + + return bPredicate; + } + + inline bool isOnNumberChar(std::u16string_view rStr, + const sal_Int32 nPos) + { + return isOnNumberChar(rStr[nPos], true/*bSignAllowed*/); + } + + bool importDoubleAndSpaces(double& o_fRetval, + sal_Int32& io_rPos, + std::u16string_view rStr, + const sal_Int32 nLen ); + + bool importFlagAndSpaces(sal_Int32& o_nRetval, + sal_Int32& io_rPos, + std::u16string_view rStr, + const sal_Int32 nLen); + +} // namespace basegfx::internal + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/basegfx/source/matrix/b2dhommatrix.cxx b/basegfx/source/matrix/b2dhommatrix.cxx new file mode 100644 index 0000000000..e4a9dda9e3 --- /dev/null +++ b/basegfx/source/matrix/b2dhommatrix.cxx @@ -0,0 +1,430 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * 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 . + */ + +#include <basegfx/matrix/b2dhommatrix.hxx> +#include <basegfx/matrix/hommatrixtemplate.hxx> +#include <basegfx/tuple/b2dtuple.hxx> +#include <basegfx/vector/b2dvector.hxx> +#include <basegfx/matrix/b2dhommatrixtools.hxx> +#include <memory> + +namespace basegfx +{ + constexpr int RowSize = 3; + + void B2DHomMatrix::set3x2(double f_0x0, double f_0x1, double f_0x2, double f_1x0, double f_1x1, double f_1x2) + { + mfValues[0][0] = f_0x0; + mfValues[0][1] = f_0x1; + mfValues[0][2] = f_0x2; + mfValues[1][0] = f_1x0; + mfValues[1][1] = f_1x1; + mfValues[1][2] = f_1x2; + } + + bool B2DHomMatrix::isIdentity() const + { + for(sal_uInt16 a(0); a < RowSize - 1; a++) + { + for(sal_uInt16 b(0); b < RowSize; b++) + { + const double fDefault(internal::implGetDefaultValue(a, b)); + const double fValueAB(get(a, b)); + + if(!::basegfx::fTools::equal(fDefault, fValueAB)) + { + return false; + } + } + } + + return true; + } + + void B2DHomMatrix::identity() + { + for(sal_uInt16 a(0); a < RowSize - 1; a++) + { + for(sal_uInt16 b(0); b < RowSize; b++) + mfValues[a][b] = internal::implGetDefaultValue(a, b); + } + } + + bool B2DHomMatrix::isInvertible() const + { + double dst[6]; + /* Compute adjoint: */ + computeAdjoint(dst); + /* Compute determinant: */ + double det = computeDeterminant(dst); + if (fTools::equalZero(det)) + return false; + return true; + } + + bool B2DHomMatrix::invert() + { + if(isIdentity()) + return true; + + double dst[6]; + + /* Compute adjoint: */ + computeAdjoint(dst); + + /* Compute determinant: */ + double det = computeDeterminant(dst); + if (fTools::equalZero(det)) + return false; + + /* Multiply adjoint with reciprocal of determinant: */ + det = 1.0 / det; + mfValues[0][0] = dst[0] * det; + mfValues[0][1] = dst[1] * det; + mfValues[0][2] = dst[2] * det; + mfValues[1][0] = dst[3] * det; + mfValues[1][1] = dst[4] * det; + mfValues[1][2] = dst[5] * det; + + return true; + } + + /* Compute adjoint, optimised for the case where the last (not stored) row is { 0, 0, 1 } */ + void B2DHomMatrix::computeAdjoint(double (&dst)[6]) const + { + dst[0] = + get(1, 1); + dst[1] = - get(0, 1); + dst[2] = + get(0, 1) * get(1, 2) - get(0, 2) * get(1, 1); + dst[3] = - get(1, 0); + dst[4] = + get(0, 0); + dst[5] = - get(0, 0) * get(1, 2) + get(0, 2) * get(1, 0); + } + + /* Compute the determinant, given the adjoint matrix */ + double B2DHomMatrix::computeDeterminant(double (&dst)[6]) const + { + return mfValues[0][0] * dst[0] + mfValues[0][1] * dst[3]; + } + + B2DHomMatrix& B2DHomMatrix::operator*=(const B2DHomMatrix& rMat) + { + if(rMat.isIdentity()) + { + // multiply with identity, no change -> nothing to do + } + else if(isIdentity()) + { + // we are identity, result will be rMat -> assign + *this = rMat; + } + else + { + // multiply + doMulMatrix(rMat); + } + + return *this; + } + + void B2DHomMatrix::doMulMatrix(const B2DHomMatrix& rMat) + { + // create a copy as source for the original values + const B2DHomMatrix aCopy(*this); + + for(sal_uInt16 a(0); a < 2; ++a) + { + for(sal_uInt16 b(0); b < 3; ++b) + { + double fValue = 0.0; + + for(sal_uInt16 c(0); c < 2; ++c) + fValue += aCopy.mfValues[c][b] * rMat.mfValues[a][c]; + + mfValues[a][b] = fValue; + } + mfValues[a][2] += rMat.mfValues[a][2]; + } + } + + bool B2DHomMatrix::operator==(const B2DHomMatrix& rMat) const + { + if (&rMat == this) + return true; + for(sal_uInt16 a(0); a < 2; a++) + { + for(sal_uInt16 b(0); b < 3; b++) + { + const double fValueA(mfValues[a][b]); + const double fValueB(rMat.mfValues[a][b]); + + if(!::basegfx::fTools::equal(fValueA, fValueB)) + { + return false; + } + } + } + return true; + } + + bool B2DHomMatrix::operator!=(const B2DHomMatrix& rMat) const + { + return !(*this == rMat); + } + + void B2DHomMatrix::rotate(double fRadiant) + { + if(fTools::equalZero(fRadiant)) + return; + + double fSin(0.0); + double fCos(1.0); + + utils::createSinCosOrthogonal(fSin, fCos, fRadiant); + B2DHomMatrix aRotMat; + + aRotMat.set(0, 0, fCos); + aRotMat.set(1, 1, fCos); + aRotMat.set(1, 0, fSin); + aRotMat.set(0, 1, -fSin); + + doMulMatrix(aRotMat); + } + + void B2DHomMatrix::translate(double fX, double fY) + { + if(!fTools::equalZero(fX) || !fTools::equalZero(fY)) + { + B2DHomMatrix aTransMat; + + aTransMat.set(0, 2, fX); + aTransMat.set(1, 2, fY); + + doMulMatrix(aTransMat); + } + } + + void B2DHomMatrix::translate(const B2DTuple& rTuple) + { + translate(rTuple.getX(), rTuple.getY()); + } + + void B2DHomMatrix::scale(double fX, double fY) + { + const double fOne(1.0); + + if(!fTools::equal(fOne, fX) || !fTools::equal(fOne, fY)) + { + B2DHomMatrix aScaleMat; + + aScaleMat.set(0, 0, fX); + aScaleMat.set(1, 1, fY); + + doMulMatrix(aScaleMat); + } + } + + void B2DHomMatrix::scale(const B2DTuple& rTuple) + { + scale(rTuple.getX(), rTuple.getY()); + } + + void B2DHomMatrix::shearX(double fSx) + { + // #i76239# do not test against 1.0, but against 0.0. We are talking about a value not on the diagonal (!) + if(!fTools::equalZero(fSx)) + { + B2DHomMatrix aShearXMat; + + aShearXMat.set(0, 1, fSx); + + doMulMatrix(aShearXMat); + } + } + + void B2DHomMatrix::shearY(double fSy) + { + // #i76239# do not test against 1.0, but against 0.0. We are talking about a value not on the diagonal (!) + if(!fTools::equalZero(fSy)) + { + B2DHomMatrix aShearYMat; + + aShearYMat.set(1, 0, fSy); + + doMulMatrix(aShearYMat); + } + } + + /** Decomposition + + New, optimized version with local shearX detection. Old version (keeping + below, is working well, too) used the 3D matrix decomposition when + shear was used. Keeping old version as comment below since it may get + necessary to add the determinant() test from there here, too. + */ + bool B2DHomMatrix::decompose(B2DTuple& rScale, B2DTuple& rTranslate, double& rRotate, double& rShearX) const + { + // reset rotate and shear and copy translation values in every case + rRotate = rShearX = 0.0; + rTranslate.setX(get(0, 2)); + rTranslate.setY(get(1, 2)); + + // test for rotation and shear + if(fTools::equalZero(get(0, 1)) && fTools::equalZero(get(1, 0))) + { + // no rotation and shear, copy scale values + rScale.setX(get(0, 0)); + rScale.setY(get(1, 1)); + + // or is there? + if( rScale.getX() < 0 && rScale.getY() < 0 ) + { + // there is - 180 degree rotated + rScale *= -1; + rRotate = M_PI; + } + } + else + { + // get the unit vectors of the transformation -> the perpendicular vectors + B2DVector aUnitVecX(get(0, 0), get(1, 0)); + B2DVector aUnitVecY(get(0, 1), get(1, 1)); + const double fScalarXY(aUnitVecX.scalar(aUnitVecY)); + + // Test if shear is zero. That's the case if the unit vectors in the matrix + // are perpendicular -> scalar is zero. This is also the case when one of + // the unit vectors is zero. + if(fTools::equalZero(fScalarXY)) + { + // calculate unsigned scale values + rScale.setX(aUnitVecX.getLength()); + rScale.setY(aUnitVecY.getLength()); + + // check unit vectors for zero lengths + const bool bXIsZero(fTools::equalZero(rScale.getX())); + const bool bYIsZero(fTools::equalZero(rScale.getY())); + + if(bXIsZero || bYIsZero) + { + // still extract as much as possible. Scalings are already set + if(!bXIsZero) + { + // get rotation of X-Axis + rRotate = atan2(aUnitVecX.getY(), aUnitVecX.getX()); + } + else if(!bYIsZero) + { + // get rotation of X-Axis. When assuming X and Y perpendicular + // and correct rotation, it's the Y-Axis rotation minus 90 degrees + rRotate = atan2(aUnitVecY.getY(), aUnitVecY.getX()) - M_PI_2; + } + + // one or both unit vectors do not exist, determinant is zero, no decomposition possible. + // Eventually used rotations or shears are lost + return false; + } + else + { + // no shear + // calculate rotation of X unit vector relative to (1, 0) + rRotate = atan2(aUnitVecX.getY(), aUnitVecX.getX()); + + // use orientation to evtl. correct sign of Y-Scale + const double fCrossXY(aUnitVecX.cross(aUnitVecY)); + + if(fCrossXY < 0.0) + { + rScale.setY(-rScale.getY()); + } + } + } + else + { + // fScalarXY is not zero, thus both unit vectors exist. No need to handle that here + // shear, extract it + double fCrossXY(aUnitVecX.cross(aUnitVecY)); + + // get rotation by calculating angle of X unit vector relative to (1, 0). + // This is before the parallel test following the motto to extract + // as much as possible + rRotate = atan2(aUnitVecX.getY(), aUnitVecX.getX()); + + // get unsigned scale value for X. It will not change and is useful + // for further corrections + rScale.setX(aUnitVecX.getLength()); + + if(fTools::equalZero(fCrossXY)) + { + // extract as much as possible + rScale.setY(aUnitVecY.getLength()); + + // unit vectors are parallel, thus not linear independent. No + // useful decomposition possible. This should not happen since + // the only way to get the unit vectors nearly parallel is + // a very big shearing. Anyways, be prepared for hand-filled + // matrices + // Eventually used rotations or shears are lost + return false; + } + else + { + // calculate the contained shear + rShearX = fScalarXY / fCrossXY; + + if(!fTools::equalZero(rRotate)) + { + // To be able to correct the shear for aUnitVecY, rotation needs to be + // removed first. Correction of aUnitVecX is easy, it will be rotated back to (1, 0). + aUnitVecX.setX(rScale.getX()); + aUnitVecX.setY(0.0); + + // for Y correction we rotate the UnitVecY back about -rRotate + const double fNegRotate(-rRotate); + const double fSin(sin(fNegRotate)); + const double fCos(cos(fNegRotate)); + + const double fNewX(aUnitVecY.getX() * fCos - aUnitVecY.getY() * fSin); + const double fNewY(aUnitVecY.getX() * fSin + aUnitVecY.getY() * fCos); + + aUnitVecY.setX(fNewX); + aUnitVecY.setY(fNewY); + } + + // Correct aUnitVecY and fCrossXY to fShear=0. Rotation is already removed. + // Shear correction can only work with removed rotation + aUnitVecY.setX(aUnitVecY.getX() - (aUnitVecY.getY() * rShearX)); + fCrossXY = aUnitVecX.cross(aUnitVecY); + + // calculate unsigned scale value for Y, after the corrections since + // the shear correction WILL change the length of aUnitVecY + rScale.setY(aUnitVecY.getLength()); + + // use orientation to set sign of Y-Scale + if(fCrossXY < 0.0) + { + rScale.setY(-rScale.getY()); + } + } + } + } + + return true; + } +} // end of namespace basegfx + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/basegfx/source/matrix/b2dhommatrixtools.cxx b/basegfx/source/matrix/b2dhommatrixtools.cxx new file mode 100644 index 0000000000..7fb6bbe0cb --- /dev/null +++ b/basegfx/source/matrix/b2dhommatrixtools.cxx @@ -0,0 +1,457 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * 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 . + */ + +#include <basegfx/matrix/b2dhommatrixtools.hxx> +#include <basegfx/range/b2drange.hxx> + +#include <osl/diagnose.h> + +namespace basegfx::utils +{ + void createSinCosOrthogonal(double& o_rSin, double& o_rCos, double fRadiant) + { + if( fTools::equalZero( fmod( fRadiant, M_PI_2 ) ) ) + { + // determine quadrant + const sal_Int32 nQuad( + (4 + fround( M_2_PI*fmod( fRadiant, 2 * M_PI ) )) % 4 ); + switch( nQuad ) + { + case 0: // -2pi,0,2pi + o_rSin = 0.0; + o_rCos = 1.0; + break; + + case 1: // -3/2pi,1/2pi + o_rSin = 1.0; + o_rCos = 0.0; + break; + + case 2: // -pi,pi + o_rSin = 0.0; + o_rCos = -1.0; + break; + + case 3: // -1/2pi,3/2pi + o_rSin = -1.0; + o_rCos = 0.0; + break; + + default: + OSL_FAIL( "createSinCos: Impossible case reached" ); + } + } + else + { + // TODO(P1): Maybe use glibc's sincos here (though + // that's kinda non-portable...) + o_rSin = sin(fRadiant); + o_rCos = cos(fRadiant); + } + } + + B2DHomMatrix createScaleB2DHomMatrix(double fScaleX, double fScaleY) + { + B2DHomMatrix aRetval; + const double fOne(1.0); + + if(!fTools::equal(fScaleX, fOne)) + { + aRetval.set(0, 0, fScaleX); + } + + if(!fTools::equal(fScaleY, fOne)) + { + aRetval.set(1, 1, fScaleY); + } + + return aRetval; + } + + B2DHomMatrix createShearXB2DHomMatrix(double fShearX) + { + B2DHomMatrix aRetval; + + if(!fTools::equalZero(fShearX)) + { + aRetval.set(0, 1, fShearX); + } + + return aRetval; + } + + B2DHomMatrix createShearYB2DHomMatrix(double fShearY) + { + B2DHomMatrix aRetval; + + if(!fTools::equalZero(fShearY)) + { + aRetval.set(1, 0, fShearY); + } + + return aRetval; + } + + B2DHomMatrix createRotateB2DHomMatrix(double fRadiant) + { + B2DHomMatrix aRetval; + + if(!fTools::equalZero(fRadiant)) + { + double fSin(0.0); + double fCos(1.0); + + createSinCosOrthogonal(fSin, fCos, fRadiant); + aRetval.set(0, 0, fCos); + aRetval.set(1, 1, fCos); + aRetval.set(1, 0, fSin); + aRetval.set(0, 1, -fSin); + } + + return aRetval; + } + + B2DHomMatrix createTranslateB2DHomMatrix(double fTranslateX, double fTranslateY) + { + B2DHomMatrix aRetval; + + if(!(fTools::equalZero(fTranslateX) && fTools::equalZero(fTranslateY))) + { + aRetval.set(0, 2, fTranslateX); + aRetval.set(1, 2, fTranslateY); + } + + return aRetval; + } + + B2DHomMatrix createScaleShearXRotateTranslateB2DHomMatrix( + double fScaleX, double fScaleY, + double fShearX, + double fRadiant, + double fTranslateX, double fTranslateY) + { + const double fOne(1.0); + + if(fTools::equal(fScaleX, fOne) && fTools::equal(fScaleY, fOne)) + { + /// no scale, take shortcut + return createShearXRotateTranslateB2DHomMatrix(fShearX, fRadiant, fTranslateX, fTranslateY); + } + else + { + /// scale used + if(fTools::equalZero(fShearX)) + { + /// no shear + if(fTools::equalZero(fRadiant)) + { + /// no rotate, take shortcut + return createScaleTranslateB2DHomMatrix(fScaleX, fScaleY, fTranslateX, fTranslateY); + } + else + { + /// rotate and scale used, no shear + double fSin(0.0); + double fCos(1.0); + + createSinCosOrthogonal(fSin, fCos, fRadiant); + + B2DHomMatrix aRetval( + /* Row 0, Column 0 */ fCos * fScaleX, + /* Row 0, Column 1 */ fScaleY * -fSin, + /* Row 0, Column 2 */ fTranslateX, + /* Row 1, Column 0 */ fSin * fScaleX, + /* Row 1, Column 1 */ fScaleY * fCos, + /* Row 1, Column 2 */ fTranslateY); + + return aRetval; + } + } + else + { + /// scale and shear used + if(fTools::equalZero(fRadiant)) + { + /// scale and shear, but no rotate + B2DHomMatrix aRetval( + /* Row 0, Column 0 */ fScaleX, + /* Row 0, Column 1 */ fScaleY * fShearX, + /* Row 0, Column 2 */ fTranslateX, + /* Row 1, Column 0 */ 0.0, + /* Row 1, Column 1 */ fScaleY, + /* Row 1, Column 2 */ fTranslateY); + + return aRetval; + } + else + { + /// scale, shear and rotate used + double fSin(0.0); + double fCos(1.0); + + createSinCosOrthogonal(fSin, fCos, fRadiant); + + B2DHomMatrix aRetval( + /* Row 0, Column 0 */ fCos * fScaleX, + /* Row 0, Column 1 */ fScaleY * ((fCos * fShearX) - fSin), + /* Row 0, Column 2 */ fTranslateX, + /* Row 1, Column 0 */ fSin * fScaleX, + /* Row 1, Column 1 */ fScaleY * ((fSin * fShearX) + fCos), + /* Row 1, Column 2 */ fTranslateY); + + return aRetval; + } + } + } + } + + B2DHomMatrix createShearXRotateTranslateB2DHomMatrix( + double fShearX, + double fRadiant, + double fTranslateX, double fTranslateY) + { + if(fTools::equalZero(fShearX)) + { + /// no shear + if(fTools::equalZero(fRadiant)) + { + /// no shear, no rotate, take shortcut + return createTranslateB2DHomMatrix(fTranslateX, fTranslateY); + } + else + { + /// no shear, but rotate used + double fSin(0.0); + double fCos(1.0); + + createSinCosOrthogonal(fSin, fCos, fRadiant); + + B2DHomMatrix aRetval( + /* Row 0, Column 0 */ fCos, + /* Row 0, Column 1 */ -fSin, + /* Row 0, Column 2 */ fTranslateX, + /* Row 1, Column 0 */ fSin, + /* Row 1, Column 1 */ fCos, + /* Row 1, Column 2 */ fTranslateY); + + return aRetval; + } + } + else + { + /// shear used + if(fTools::equalZero(fRadiant)) + { + /// no rotate, but shear used + B2DHomMatrix aRetval( + /* Row 0, Column 0 */ 1.0, + /* Row 0, Column 1 */ fShearX, + /* Row 0, Column 2 */ fTranslateX, + /* Row 1, Column 0 */ 0.0, + /* Row 1, Column 1 */ 1.0, + /* Row 1, Column 2 */ fTranslateY); + + return aRetval; + } + else + { + /// shear and rotate used + double fSin(0.0); + double fCos(1.0); + + createSinCosOrthogonal(fSin, fCos, fRadiant); + + B2DHomMatrix aRetval( + /* Row 0, Column 0 */ fCos, + /* Row 0, Column 1 */ (fCos * fShearX) - fSin, + /* Row 0, Column 2 */ fTranslateX, + /* Row 1, Column 0 */ fSin, + /* Row 1, Column 1 */ (fSin * fShearX) + fCos, + /* Row 1, Column 2 */ fTranslateY); + + return aRetval; + } + } + } + + B2DHomMatrix createScaleTranslateB2DHomMatrix( + double fScaleX, double fScaleY, + double fTranslateX, double fTranslateY) + { + const double fOne(1.0); + + if(fTools::equal(fScaleX, fOne) && fTools::equal(fScaleY, fOne)) + { + /// no scale, take shortcut + return createTranslateB2DHomMatrix(fTranslateX, fTranslateY); + } + else + { + /// scale used + if(fTools::equalZero(fTranslateX) && fTools::equalZero(fTranslateY)) + { + /// no translate, but scale. + B2DHomMatrix aRetval; + + aRetval.set(0, 0, fScaleX); + aRetval.set(1, 1, fScaleY); + + return aRetval; + } + else + { + /// translate and scale + B2DHomMatrix aRetval( + /* Row 0, Column 0 */ fScaleX, + /* Row 0, Column 1 */ 0.0, + /* Row 0, Column 2 */ fTranslateX, + /* Row 1, Column 0 */ 0.0, + /* Row 1, Column 1 */ fScaleY, + /* Row 1, Column 2 */ fTranslateY); + + return aRetval; + } + } + } + + B2DHomMatrix createRotateAroundPoint( + double fPointX, double fPointY, + double fRadiant) + { + B2DHomMatrix aRetval; + + if(!fTools::equalZero(fRadiant)) + { + double fSin(0.0); + double fCos(1.0); + + createSinCosOrthogonal(fSin, fCos, fRadiant); + + aRetval.set3x2( + /* Row 0, Column 0 */ fCos, + /* Row 0, Column 1 */ -fSin, + /* Row 0, Column 2 */ (fPointX * (1.0 - fCos)) + (fSin * fPointY), + /* Row 1, Column 0 */ fSin, + /* Row 1, Column 1 */ fCos, + /* Row 1, Column 2 */ (fPointY * (1.0 - fCos)) - (fSin * fPointX)); + } + + return aRetval; + } + + B2DHomMatrix createRotateAroundCenterKeepAspectRatioStayInsideRange( + const basegfx::B2DRange& rTargetRange, + double fRotate) + { + basegfx::B2DHomMatrix aRetval; + + // RotGrfFlyFrame: Create a transformation that maps the range inside of itself + // so that it fits, takes as much space as possible and keeps the aspect ratio + if(0.0 != fRotate) + { + // Fit rotated graphic to center of available space, keeping page ratio: + // Adapt scaling ratio of unit object and rotate it + aRetval.scale(1.0, rTargetRange.getHeight() / rTargetRange.getWidth()); + aRetval.rotate(fRotate); + + // get the range to see where we are in unit coordinates + basegfx::B2DRange aFullRange(0.0, 0.0, 1.0, 1.0); + aFullRange.transform(aRetval); + + // detect needed scales in X/Y and choose the smallest for staying inside the + // available space while keeping aspect ratio of the source + const double fScaleX(rTargetRange.getWidth() / aFullRange.getWidth()); + const double fScaleY(rTargetRange.getHeight() / aFullRange.getHeight()); + const double fScaleMin(std::min(fScaleX, fScaleY)); + + // TopLeft to zero, then scale, then move to center of available space + aRetval.translate(-aFullRange.getMinX(), -aFullRange.getMinY()); + aRetval.scale(fScaleMin, fScaleMin); + aRetval.translate( + rTargetRange.getCenterX() - (0.5 * fScaleMin * aFullRange.getWidth()), + rTargetRange.getCenterY() - (0.5 * fScaleMin * aFullRange.getHeight())); + } + else + { + // just scale/translate needed + aRetval *= createScaleTranslateB2DHomMatrix( + rTargetRange.getRange(), + rTargetRange.getMinimum()); + } + + return aRetval; + } + + /// special for the case to map from source range to target range + B2DHomMatrix createSourceRangeTargetRangeTransform( + const B2DRange& rSourceRange, + const B2DRange& rTargetRange) + { + B2DHomMatrix aRetval; + + if(&rSourceRange == &rTargetRange) + { + return aRetval; + } + + if(!fTools::equalZero(rSourceRange.getMinX()) || !fTools::equalZero(rSourceRange.getMinY())) + { + aRetval.set(0, 2, -rSourceRange.getMinX()); + aRetval.set(1, 2, -rSourceRange.getMinY()); + } + + const double fSourceW(rSourceRange.getWidth()); + const double fSourceH(rSourceRange.getHeight()); + const bool bDivX(!fTools::equalZero(fSourceW) && !fTools::equal(fSourceW, 1.0)); + const bool bDivY(!fTools::equalZero(fSourceH) && !fTools::equal(fSourceH, 1.0)); + const double fScaleX(bDivX ? rTargetRange.getWidth() / fSourceW : rTargetRange.getWidth()); + const double fScaleY(bDivY ? rTargetRange.getHeight() / fSourceH : rTargetRange.getHeight()); + + if(!fTools::equalZero(fScaleX) || !fTools::equalZero(fScaleY)) + { + aRetval.scale(fScaleX, fScaleY); + } + + if(!fTools::equalZero(rTargetRange.getMinX()) || !fTools::equalZero(rTargetRange.getMinY())) + { + aRetval.translate( + rTargetRange.getMinX(), + rTargetRange.getMinY()); + } + + return aRetval; + } + + B2DHomMatrix createCoordinateSystemTransform( + const B2DPoint& rOrigin, + const B2DVector& rX, + const B2DVector& rY) + { + return basegfx::B2DHomMatrix( + rX.getX(), rY.getX(), rOrigin.getX(), + rX.getY(), rY.getY(), rOrigin.getY()); + } + + B2DTuple getColumn(const B2DHomMatrix& rMatrix, sal_uInt16 nCol) + { + return B2DTuple(rMatrix.get(0, nCol), rMatrix.get(1, nCol)); + } +} // end of namespace + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/basegfx/source/matrix/b3dhommatrix.cxx b/basegfx/source/matrix/b3dhommatrix.cxx new file mode 100644 index 0000000000..f9018c6979 --- /dev/null +++ b/basegfx/source/matrix/b3dhommatrix.cxx @@ -0,0 +1,546 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * 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 . + */ + +#include <basegfx/matrix/b3dhommatrix.hxx> +#include <basegfx/matrix/hommatrixtemplate.hxx> +#include <basegfx/vector/b3dvector.hxx> +#include <memory> + +namespace basegfx +{ + typedef ::basegfx::internal::ImplHomMatrixTemplate< 4 >Impl3DHomMatrix_Base; + class Impl3DHomMatrix : public Impl3DHomMatrix_Base + { + }; + + B3DHomMatrix::B3DHomMatrix() = default; + + B3DHomMatrix::B3DHomMatrix(const B3DHomMatrix&) = default; + + B3DHomMatrix::B3DHomMatrix(B3DHomMatrix&&) = default; + + B3DHomMatrix::~B3DHomMatrix() = default; + + B3DHomMatrix& B3DHomMatrix::operator=(const B3DHomMatrix&) = default; + + B3DHomMatrix& B3DHomMatrix::operator=(B3DHomMatrix&&) = default; + + double B3DHomMatrix::get(sal_uInt16 nRow, sal_uInt16 nColumn) const + { + return mpImpl->get(nRow, nColumn); + } + + void B3DHomMatrix::set(sal_uInt16 nRow, sal_uInt16 nColumn, double fValue) + { + mpImpl->set(nRow, nColumn, fValue); + } + + bool B3DHomMatrix::isLastLineDefault() const + { + return mpImpl->isLastLineDefault(); + } + + bool B3DHomMatrix::isIdentity() const + { + return mpImpl->isIdentity(); + } + + void B3DHomMatrix::identity() + { + *mpImpl = Impl3DHomMatrix(); + } + + void B3DHomMatrix::invert() + { + Impl3DHomMatrix aWork(*mpImpl); + std::unique_ptr<sal_uInt16[]> pIndex( new sal_uInt16[Impl3DHomMatrix_Base::getEdgeLength()] ); + sal_Int16 nParity; + + if(aWork.ludcmp(pIndex.get(), nParity)) + { + mpImpl->doInvert(aWork, pIndex.get()); + } + } + + double B3DHomMatrix::determinant() const + { + return mpImpl->doDeterminant(); + } + + B3DHomMatrix& B3DHomMatrix::operator+=(const B3DHomMatrix& rMat) + { + mpImpl->doAddMatrix(*rMat.mpImpl); + return *this; + } + + B3DHomMatrix& B3DHomMatrix::operator-=(const B3DHomMatrix& rMat) + { + mpImpl->doSubMatrix(*rMat.mpImpl); + return *this; + } + + B3DHomMatrix& B3DHomMatrix::operator*=(double fValue) + { + const double fOne(1.0); + + if(!fTools::equal(fOne, fValue)) + mpImpl->doMulMatrix(fValue); + + return *this; + } + + B3DHomMatrix& B3DHomMatrix::operator/=(double fValue) + { + const double fOne(1.0); + + if(!fTools::equal(fOne, fValue)) + mpImpl->doMulMatrix(1.0 / fValue); + + return *this; + } + + B3DHomMatrix& B3DHomMatrix::operator*=(const B3DHomMatrix& rMat) + { + if(rMat.isIdentity()) + { + // multiply with identity, no change -> nothing to do + } + else if(isIdentity()) + { + // we are identity, result will be rMat -> assign + *this = rMat; + } + else + { + // multiply + mpImpl->doMulMatrix(*rMat.mpImpl); + } + return *this; + } + + bool B3DHomMatrix::operator==(const B3DHomMatrix& rMat) const + { + if(mpImpl.same_object(rMat.mpImpl)) + return true; + + return mpImpl->isEqual(*rMat.mpImpl); + } + + bool B3DHomMatrix::operator!=(const B3DHomMatrix& rMat) const + { + return !(*this == rMat); + } + + void B3DHomMatrix::rotate(double fAngleX,double fAngleY,double fAngleZ) + { + if(fTools::equalZero(fAngleX) && fTools::equalZero(fAngleY) && fTools::equalZero(fAngleZ)) + return; + + if(!fTools::equalZero(fAngleX)) + { + Impl3DHomMatrix aRotMatX; + double fSin(sin(fAngleX)); + double fCos(cos(fAngleX)); + + aRotMatX.set(1, 1, fCos); + aRotMatX.set(2, 2, fCos); + aRotMatX.set(2, 1, fSin); + aRotMatX.set(1, 2, -fSin); + + mpImpl->doMulMatrix(aRotMatX); + } + + if(!fTools::equalZero(fAngleY)) + { + Impl3DHomMatrix aRotMatY; + double fSin(sin(fAngleY)); + double fCos(cos(fAngleY)); + + aRotMatY.set(0, 0, fCos); + aRotMatY.set(2, 2, fCos); + aRotMatY.set(0, 2, fSin); + aRotMatY.set(2, 0, -fSin); + + mpImpl->doMulMatrix(aRotMatY); + } + + if(fTools::equalZero(fAngleZ)) + return; + + Impl3DHomMatrix aRotMatZ; + double fSin(sin(fAngleZ)); + double fCos(cos(fAngleZ)); + + aRotMatZ.set(0, 0, fCos); + aRotMatZ.set(1, 1, fCos); + aRotMatZ.set(1, 0, fSin); + aRotMatZ.set(0, 1, -fSin); + + mpImpl->doMulMatrix(aRotMatZ); + } + + void B3DHomMatrix::rotate(const B3DTuple& rRotation) + { + rotate(rRotation.getX(), rRotation.getY(), rRotation.getZ()); + } + + void B3DHomMatrix::translate(double fX, double fY, double fZ) + { + if(!fTools::equalZero(fX) || !fTools::equalZero(fY) || !fTools::equalZero(fZ)) + { + Impl3DHomMatrix aTransMat; + + aTransMat.set(0, 3, fX); + aTransMat.set(1, 3, fY); + aTransMat.set(2, 3, fZ); + + mpImpl->doMulMatrix(aTransMat); + } + } + + void B3DHomMatrix::translate(const B3DTuple& rRotation) + { + translate(rRotation.getX(), rRotation.getY(), rRotation.getZ()); + } + + void B3DHomMatrix::scale(double fX, double fY, double fZ) + { + const double fOne(1.0); + + if(!fTools::equal(fOne, fX) || !fTools::equal(fOne, fY) ||!fTools::equal(fOne, fZ)) + { + Impl3DHomMatrix aScaleMat; + + aScaleMat.set(0, 0, fX); + aScaleMat.set(1, 1, fY); + aScaleMat.set(2, 2, fZ); + + mpImpl->doMulMatrix(aScaleMat); + } + } + + void B3DHomMatrix::scale(const B3DTuple& rRotation) + { + scale(rRotation.getX(), rRotation.getY(), rRotation.getZ()); + } + + void B3DHomMatrix::shearXY(double fSx, double fSy) + { + // #i76239# do not test against 1.0, but against 0.0. We are talking about a value not on the diagonal (!) + if(!fTools::equalZero(fSx) || !fTools::equalZero(fSy)) + { + Impl3DHomMatrix aShearXYMat; + + aShearXYMat.set(0, 2, fSx); + aShearXYMat.set(1, 2, fSy); + + mpImpl->doMulMatrix(aShearXYMat); + } + } + + void B3DHomMatrix::shearXZ(double fSx, double fSz) + { + // #i76239# do not test against 1.0, but against 0.0. We are talking about a value not on the diagonal (!) + if(!fTools::equalZero(fSx) || !fTools::equalZero(fSz)) + { + Impl3DHomMatrix aShearXZMat; + + aShearXZMat.set(0, 1, fSx); + aShearXZMat.set(2, 1, fSz); + + mpImpl->doMulMatrix(aShearXZMat); + } + } + void B3DHomMatrix::frustum(double fLeft, double fRight, double fBottom, double fTop, double fNear, double fFar) + { + const double fZero(0.0); + const double fOne(1.0); + + if(!fTools::more(fNear, fZero)) + { + fNear = 0.001; + } + + if(!fTools::more(fFar, fZero)) + { + fFar = fOne; + } + + if(fTools::equal(fNear, fFar)) + { + fFar = fNear + fOne; + } + + if(fTools::equal(fLeft, fRight)) + { + fLeft -= fOne; + fRight += fOne; + } + + if(fTools::equal(fTop, fBottom)) + { + fBottom -= fOne; + fTop += fOne; + } + + Impl3DHomMatrix aFrustumMat; + + aFrustumMat.set(0, 0, 2.0 * fNear / (fRight - fLeft)); + aFrustumMat.set(1, 1, 2.0 * fNear / (fTop - fBottom)); + aFrustumMat.set(0, 2, (fRight + fLeft) / (fRight - fLeft)); + aFrustumMat.set(1, 2, (fTop + fBottom) / (fTop - fBottom)); + aFrustumMat.set(2, 2, -fOne * ((fFar + fNear) / (fFar - fNear))); + aFrustumMat.set(3, 2, -fOne); + aFrustumMat.set(2, 3, -fOne * ((2.0 * fFar * fNear) / (fFar - fNear))); + aFrustumMat.set(3, 3, fZero); + + mpImpl->doMulMatrix(aFrustumMat); + } + + void B3DHomMatrix::ortho(double fLeft, double fRight, double fBottom, double fTop, double fNear, double fFar) + { + if(fTools::equal(fNear, fFar)) + { + fFar = fNear + 1.0; + } + + if(fTools::equal(fLeft, fRight)) + { + fLeft -= 1.0; + fRight += 1.0; + } + + if(fTools::equal(fTop, fBottom)) + { + fBottom -= 1.0; + fTop += 1.0; + } + + Impl3DHomMatrix aOrthoMat; + + aOrthoMat.set(0, 0, 2.0 / (fRight - fLeft)); + aOrthoMat.set(1, 1, 2.0 / (fTop - fBottom)); + aOrthoMat.set(2, 2, -1.0 * (2.0 / (fFar - fNear))); + aOrthoMat.set(0, 3, -1.0 * ((fRight + fLeft) / (fRight - fLeft))); + aOrthoMat.set(1, 3, -1.0 * ((fTop + fBottom) / (fTop - fBottom))); + aOrthoMat.set(2, 3, -1.0 * ((fFar + fNear) / (fFar - fNear))); + + mpImpl->doMulMatrix(aOrthoMat); + } + + void B3DHomMatrix::orientation(const B3DPoint& rVRP, B3DVector aVPN, B3DVector aVUV) + { + Impl3DHomMatrix aOrientationMat; + + // translate -VRP + aOrientationMat.set(0, 3, -rVRP.getX()); + aOrientationMat.set(1, 3, -rVRP.getY()); + aOrientationMat.set(2, 3, -rVRP.getZ()); + + // build rotation + aVUV.normalize(); + aVPN.normalize(); + + // build x-axis as perpendicular from aVUV and aVPN + B3DVector aRx(aVUV.getPerpendicular(aVPN)); + aRx.normalize(); + + // y-axis perpendicular to that + B3DVector aRy(aVPN.getPerpendicular(aRx)); + aRy.normalize(); + + // the calculated normals are the line vectors of the rotation matrix, + // set them to create rotation + aOrientationMat.set(0, 0, aRx.getX()); + aOrientationMat.set(0, 1, aRx.getY()); + aOrientationMat.set(0, 2, aRx.getZ()); + aOrientationMat.set(1, 0, aRy.getX()); + aOrientationMat.set(1, 1, aRy.getY()); + aOrientationMat.set(1, 2, aRy.getZ()); + aOrientationMat.set(2, 0, aVPN.getX()); + aOrientationMat.set(2, 1, aVPN.getY()); + aOrientationMat.set(2, 2, aVPN.getZ()); + + mpImpl->doMulMatrix(aOrientationMat); + } + + void B3DHomMatrix::decompose(B3DTuple& rScale, B3DTuple& rTranslate, B3DTuple& rRotate, B3DTuple& rShear) const + { + // when perspective is used, decompose is not made here + if(!mpImpl->isLastLineDefault()) + return; + + // If determinant is zero, decomposition is not possible + if(determinant() == 0.0) + return; + + // isolate translation + rTranslate.setX(mpImpl->get(0, 3)); + rTranslate.setY(mpImpl->get(1, 3)); + rTranslate.setZ(mpImpl->get(2, 3)); + + // correct translate values + rTranslate.correctValues(); + + // get scale and shear + B3DVector aCol0(mpImpl->get(0, 0), mpImpl->get(1, 0), mpImpl->get(2, 0)); + B3DVector aCol1(mpImpl->get(0, 1), mpImpl->get(1, 1), mpImpl->get(2, 1)); + B3DVector aCol2(mpImpl->get(0, 2), mpImpl->get(1, 2), mpImpl->get(2, 2)); + B3DVector aTemp; + + // get ScaleX + rScale.setX(aCol0.getLength()); + aCol0.normalize(); + + // get ShearXY + rShear.setX(aCol0.scalar(aCol1)); + + if(fTools::equalZero(rShear.getX())) + { + rShear.setX(0.0); + } + else + { + aTemp.setX(aCol1.getX() - rShear.getX() * aCol0.getX()); + aTemp.setY(aCol1.getY() - rShear.getX() * aCol0.getY()); + aTemp.setZ(aCol1.getZ() - rShear.getX() * aCol0.getZ()); + aCol1 = aTemp; + } + + // get ScaleY + rScale.setY(aCol1.getLength()); + aCol1.normalize(); + + const double fShearX(rShear.getX()); + + if(!fTools::equalZero(fShearX)) + { + rShear.setX(rShear.getX() / rScale.getY()); + } + + // get ShearXZ + rShear.setY(aCol0.scalar(aCol2)); + + if(fTools::equalZero(rShear.getY())) + { + rShear.setY(0.0); + } + else + { + aTemp.setX(aCol2.getX() - rShear.getY() * aCol0.getX()); + aTemp.setY(aCol2.getY() - rShear.getY() * aCol0.getY()); + aTemp.setZ(aCol2.getZ() - rShear.getY() * aCol0.getZ()); + aCol2 = aTemp; + } + + // get ShearYZ + rShear.setZ(aCol1.scalar(aCol2)); + + if(fTools::equalZero(rShear.getZ())) + { + rShear.setZ(0.0); + } + else + { + aTemp.setX(aCol2.getX() - rShear.getZ() * aCol1.getX()); + aTemp.setY(aCol2.getY() - rShear.getZ() * aCol1.getY()); + aTemp.setZ(aCol2.getZ() - rShear.getZ() * aCol1.getZ()); + aCol2 = aTemp; + } + + // get ScaleZ + rScale.setZ(aCol2.getLength()); + aCol2.normalize(); + + const double fShearY(rShear.getY()); + + if(!fTools::equalZero(fShearY)) + { + // coverity[copy_paste_error : FALSE] - this is correct getZ, not getY + rShear.setY(rShear.getY() / rScale.getZ()); + } + + const double fShearZ(rShear.getZ()); + + if(!fTools::equalZero(fShearZ)) + { + // coverity[original] - this is not an original copy-and-paste source for ^^^ + rShear.setZ(rShear.getZ() / rScale.getZ()); + } + + // correct shear values + rShear.correctValues(); + + // Coordinate system flip? + if(0.0 > aCol0.scalar(aCol1.getPerpendicular(aCol2))) + { + rScale = -rScale; + aCol0 = -aCol0; + aCol1 = -aCol1; + aCol2 = -aCol2; + } + + // correct scale values + rScale.correctValues(1.0); + + // Get rotations + { + double fy=0; + double cy=0; + + if( ::basegfx::fTools::equal( aCol0.getZ(), 1.0 ) + || aCol0.getZ() > 1.0 ) + { + fy = -M_PI/2.0; + cy = 0.0; + } + else if( ::basegfx::fTools::equal( aCol0.getZ(), -1.0 ) + || aCol0.getZ() < -1.0 ) + { + fy = M_PI/2.0; + cy = 0.0; + } + else + { + fy = asin( -aCol0.getZ() ); + cy = cos(fy); + } + + rRotate.setY(fy); + if( ::basegfx::fTools::equalZero( cy ) ) + { + if( aCol0.getZ() > 0.0 ) + rRotate.setX(atan2(-1.0*aCol1.getX(), aCol1.getY())); + else + rRotate.setX(atan2(aCol1.getX(), aCol1.getY())); + rRotate.setZ(0.0); + } + else + { + rRotate.setX(atan2(aCol1.getZ(), aCol2.getZ())); + rRotate.setZ(atan2(aCol0.getY(), aCol0.getX())); + } + + // correct rotate values + rRotate.correctValues(); + } + } +} // end of namespace basegfx + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/basegfx/source/matrix/b3dhommatrixtools.cxx b/basegfx/source/matrix/b3dhommatrixtools.cxx new file mode 100644 index 0000000000..9125aca4e0 --- /dev/null +++ b/basegfx/source/matrix/b3dhommatrixtools.cxx @@ -0,0 +1,71 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * 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 . + */ + +#include <basegfx/matrix/b3dhommatrixtools.hxx> +#include <com/sun/star/drawing/HomogenMatrix.hpp> + +namespace basegfx::utils +{ +B3DHomMatrix UnoHomogenMatrixToB3DHomMatrix(const com::sun::star::drawing::HomogenMatrix& rMatrixIn) +{ + B3DHomMatrix aRetval; + + aRetval.set(0, 0, rMatrixIn.Line1.Column1); + aRetval.set(0, 1, rMatrixIn.Line1.Column2); + aRetval.set(0, 2, rMatrixIn.Line1.Column3); + aRetval.set(0, 3, rMatrixIn.Line1.Column4); + aRetval.set(1, 0, rMatrixIn.Line2.Column1); + aRetval.set(1, 1, rMatrixIn.Line2.Column2); + aRetval.set(1, 2, rMatrixIn.Line2.Column3); + aRetval.set(1, 3, rMatrixIn.Line2.Column4); + aRetval.set(2, 0, rMatrixIn.Line3.Column1); + aRetval.set(2, 1, rMatrixIn.Line3.Column2); + aRetval.set(2, 2, rMatrixIn.Line3.Column3); + aRetval.set(2, 3, rMatrixIn.Line3.Column4); + aRetval.set(3, 0, rMatrixIn.Line4.Column1); + aRetval.set(3, 1, rMatrixIn.Line4.Column2); + aRetval.set(3, 2, rMatrixIn.Line4.Column3); + aRetval.set(3, 3, rMatrixIn.Line4.Column4); + + return aRetval; +} + +void B3DHomMatrixToUnoHomogenMatrix(const B3DHomMatrix& rMatrixIn, + com::sun::star::drawing::HomogenMatrix& rMatrixOut) +{ + rMatrixOut.Line1.Column1 = rMatrixIn.get(0, 0); + rMatrixOut.Line1.Column2 = rMatrixIn.get(0, 1); + rMatrixOut.Line1.Column3 = rMatrixIn.get(0, 2); + rMatrixOut.Line1.Column4 = rMatrixIn.get(0, 3); + rMatrixOut.Line2.Column1 = rMatrixIn.get(1, 0); + rMatrixOut.Line2.Column2 = rMatrixIn.get(1, 1); + rMatrixOut.Line2.Column3 = rMatrixIn.get(1, 2); + rMatrixOut.Line2.Column4 = rMatrixIn.get(1, 3); + rMatrixOut.Line3.Column1 = rMatrixIn.get(2, 0); + rMatrixOut.Line3.Column2 = rMatrixIn.get(2, 1); + rMatrixOut.Line3.Column3 = rMatrixIn.get(2, 2); + rMatrixOut.Line3.Column4 = rMatrixIn.get(2, 3); + rMatrixOut.Line4.Column1 = rMatrixIn.get(3, 0); + rMatrixOut.Line4.Column2 = rMatrixIn.get(3, 1); + rMatrixOut.Line4.Column3 = rMatrixIn.get(3, 2); + rMatrixOut.Line4.Column4 = rMatrixIn.get(3, 3); +} +} // end of namespace + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/basegfx/source/numeric/ftools.cxx b/basegfx/source/numeric/ftools.cxx new file mode 100644 index 0000000000..246d8d548a --- /dev/null +++ b/basegfx/source/numeric/ftools.cxx @@ -0,0 +1,135 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * 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 . + */ + +#include <basegfx/numeric/ftools.hxx> + +namespace basegfx +{ + double snapToNearestMultiple(double v, const double fStep) + { + if(fTools::equalZero(fStep)) + { + // with a zero step, all snaps to 0.0 + return 0.0; + } + else + { + const double fHalfStep(fStep * 0.5); + const double fChange(fHalfStep - fmod(v + fHalfStep, fStep)); + + if(basegfx::fTools::equal(fabs(v), fabs(fChange))) + { + return 0.0; + } + else + { + return v + fChange; + } + } + } + + double snapToZeroRange(double v, double fWidth) + { + if(fTools::equalZero(fWidth)) + { + // with no range all snaps to range bound + return 0.0; + } + else + { + if(v < 0.0 || v > fWidth) + { + double fRetval(fmod(v, fWidth)); + + if(fRetval < 0.0) + { + fRetval += fWidth; + } + + return fRetval; + } + else + { + return v; + } + } + } + + double snapToRange(double v, double fLow, double fHigh) + { + if(fTools::equal(fLow, fHigh)) + { + // with no range all snaps to range bound + return 0.0; + } + else + { + if(fLow > fHigh) + { + // correct range order. Evtl. assert this (?) + std::swap(fLow, fHigh); + } + + if(v < fLow || v > fHigh) + { + return snapToZeroRange(v - fLow, fHigh - fLow) + fLow; + } + else + { + return v; + } + } + } + + double normalizeToRange(double v, const double fRange) + { + if(fTools::lessOrEqual(fRange, 0.0)) + { + // with a zero (or less) range, all normalizes to 0.0 + return 0.0; + } + + const bool bNegative(fTools::less(v, 0.0)); + + if(bNegative) + { + if(fTools::moreOrEqual(v, -fRange)) + { + // in range [-fRange, 0.0[, shift one step + return v + fRange; + } + + // re-calculate + return v - (floor(v/fRange)*fRange); + } + else + { + if(fTools::less(v, fRange)) + { + // already in range [0.0, fRange[, nothing to do + return v; + } + + // re-calculate + return v - (floor(v/fRange)*fRange); + } + } +} // end of namespace basegfx + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/basegfx/source/point/b2dpoint.cxx b/basegfx/source/point/b2dpoint.cxx new file mode 100644 index 0000000000..d12c3dd04c --- /dev/null +++ b/basegfx/source/point/b2dpoint.cxx @@ -0,0 +1,51 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * 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 . + */ + +#include <basegfx/point/b2dpoint.hxx> +#include <basegfx/matrix/b2dhommatrix.hxx> +#include <basegfx/numeric/ftools.hxx> + +namespace basegfx +{ + B2DPoint& B2DPoint::operator*=( const ::basegfx::B2DHomMatrix& rMat ) + { + double fTempX( + rMat.get(0, 0) * mnX + + rMat.get(0, 1) * mnY + + rMat.get(0, 2)); + double fTempY( + rMat.get(1, 0) * mnX + + rMat.get(1, 1) * mnY + + rMat.get(1, 2)); + + mnX = fTempX; + mnY = fTempY; + + return *this; + } + + B2DPoint operator*( const ::basegfx::B2DHomMatrix& rMat, const B2DPoint& rPoint ) + { + B2DPoint aRes( rPoint ); + aRes *= rMat; + return aRes; + } +} // end of namespace basegfx + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/basegfx/source/point/b2ipoint.cxx b/basegfx/source/point/b2ipoint.cxx new file mode 100644 index 0000000000..2790fe3a18 --- /dev/null +++ b/basegfx/source/point/b2ipoint.cxx @@ -0,0 +1,51 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * 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 . + */ + +#include <basegfx/point/b2ipoint.hxx> +#include <basegfx/matrix/b2dhommatrix.hxx> +#include <basegfx/numeric/ftools.hxx> + +namespace basegfx +{ + B2IPoint& B2IPoint::operator=( const ::basegfx::B2ITuple& rPoint ) + { + mnX = rPoint.getX(); + mnY = rPoint.getY(); + return *this; + } + + B2IPoint& B2IPoint::operator*=( const ::basegfx::B2DHomMatrix& rMat ) + { + double fTempX( + rMat.get(0, 0) * mnX + + rMat.get(0, 1) * mnY + + rMat.get(0, 2)); + double fTempY( + rMat.get(1, 0) * mnX + + rMat.get(1, 1) * mnY + + rMat.get(1, 2)); + + mnX = fround(fTempX); + mnY = fround(fTempY); + + return *this; + } +} // end of namespace basegfx + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/basegfx/source/point/b3dpoint.cxx b/basegfx/source/point/b3dpoint.cxx new file mode 100644 index 0000000000..7d2d4cf4a2 --- /dev/null +++ b/basegfx/source/point/b3dpoint.cxx @@ -0,0 +1,76 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * 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 . + */ + +#include <basegfx/point/b3dpoint.hxx> +#include <basegfx/matrix/b3dhommatrix.hxx> +#include <basegfx/numeric/ftools.hxx> + +namespace basegfx +{ + B3DPoint& B3DPoint::operator*=( const ::basegfx::B3DHomMatrix& rMat ) + { + double fTempX( + rMat.get(0, 0) * mnX + + rMat.get(0, 1) * mnY + + rMat.get(0, 2) * mnZ + + rMat.get(0, 3)); + double fTempY( + rMat.get(1, 0) * mnX + + rMat.get(1, 1) * mnY + + rMat.get(1, 2) * mnZ + + rMat.get(1, 3)); + double fTempZ( + rMat.get(2, 0) * mnX + + rMat.get(2, 1) * mnY + + rMat.get(2, 2) * mnZ + + rMat.get(2, 3)); + + if(!rMat.isLastLineDefault()) + { + const double fOne(1.0); + const double fTempM( + rMat.get(3, 0) * mnX + + rMat.get(3, 1) * mnY + + rMat.get(3, 2) * mnZ + + rMat.get(3, 3)); + + if(!fTools::equalZero(fTempM) && !fTools::equal(fOne, fTempM)) + { + fTempX /= fTempM; + fTempY /= fTempM; + fTempZ /= fTempM; + } + } + + mnX = fTempX; + mnY = fTempY; + mnZ = fTempZ; + + return *this; + } + + B3DPoint operator*( const ::basegfx::B3DHomMatrix& rMat, const B3DPoint& rPoint ) + { + B3DPoint aRes( rPoint ); + aRes *= rMat; + return aRes; + } +} // end of namespace basegfx + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/basegfx/source/polygon/WaveLine.cxx b/basegfx/source/polygon/WaveLine.cxx new file mode 100644 index 0000000000..bc85c45362 --- /dev/null +++ b/basegfx/source/polygon/WaveLine.cxx @@ -0,0 +1,52 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + */ + +#include <basegfx/polygon/WaveLine.hxx> +#include <basegfx/point/b2dpoint.hxx> + +namespace basegfx +{ +BASEGFX_DLLPUBLIC B2DPolygon createWaveLinePolygon(basegfx::B2DRectangle const& rRectangle) +{ + basegfx::B2DPolygon aPolygon; + + double fWaveHeight = rRectangle.getHeight(); + // Wavelength depends on the wave height so it looks nice + double fHalfWaveLength = fWaveHeight + 1.0; + double fWaveAmplitude = fWaveHeight / 2.0; + + double fLastX = rRectangle.getMinX(); + double fBaseY = rRectangle.getMinY() + fWaveAmplitude; + double fDirection = 1.0; + + // In quadratic bezier the curve is 1/2 of the control height + // so we need to compensate for that. + constexpr double fHeightCompensation = 2.0; + + aPolygon.append(basegfx::B2DPoint(fLastX, fBaseY)); + + for (double fI = fHalfWaveLength; fI <= rRectangle.getWidth(); fI += fHalfWaveLength) + { + basegfx::B2DPoint aPoint(fLastX + fHalfWaveLength, fBaseY); + basegfx::B2DPoint aControl(fLastX + (fHalfWaveLength / 2.0), + fBaseY + fDirection * fWaveAmplitude * fHeightCompensation); + + aPolygon.appendQuadraticBezierSegment(aControl, aPoint); + + fLastX = aPoint.getX(); // next iteration + fDirection *= -1.0; // fDirection iterates between 1 and -1 + } + + return aPolygon; +} + +} // end of namespace basegfx + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/basegfx/source/polygon/b2dlinegeometry.cxx b/basegfx/source/polygon/b2dlinegeometry.cxx new file mode 100644 index 0000000000..437ebcbb49 --- /dev/null +++ b/basegfx/source/polygon/b2dlinegeometry.cxx @@ -0,0 +1,998 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * 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 . + */ + +#include <osl/diagnose.h> +#include <sal/log.hxx> +#include <basegfx/polygon/b2dlinegeometry.hxx> +#include <basegfx/point/b2dpoint.hxx> +#include <basegfx/vector/b2dvector.hxx> +#include <basegfx/polygon/b2dpolygontools.hxx> +#include <basegfx/polygon/b2dpolypolygontools.hxx> +#include <basegfx/range/b2drange.hxx> +#include <basegfx/matrix/b2dhommatrix.hxx> +#include <basegfx/curve/b2dcubicbezier.hxx> +#include <basegfx/matrix/b2dhommatrixtools.hxx> +#include <com/sun/star/drawing/LineCap.hpp> +#include <basegfx/polygon/b2dpolypolygoncutter.hxx> +#include <basegfx/polygon/b2dpolygontriangulator.hxx> + +namespace basegfx::utils +{ + B2DPolyPolygon createAreaGeometryForLineStartEnd( + const B2DPolygon& rCandidate, + const B2DPolyPolygon& rArrow, + bool bStart, + double fWidth, + double fCandidateLength, + double fDockingPosition, // 0->top, 1->bottom + double* pConsumedLength, + double fShift) + { + B2DPolyPolygon aRetval; + assert((rCandidate.count() > 1) && "createAreaGeometryForLineStartEnd: Line polygon has too few points"); + assert((rArrow.count() > 0) && "createAreaGeometryForLineStartEnd: Empty arrow utils::PolyPolygon"); + assert((fWidth > 0.0) && "createAreaGeometryForLineStartEnd: Width too small"); + assert((fDockingPosition >= 0.0 && fDockingPosition <= 1.0) && + "createAreaGeometryForLineStartEnd: fDockingPosition out of range [0.0 .. 1.0]"); + + if(fWidth < 0.0) + { + fWidth = -fWidth; + } + + if(rCandidate.count() > 1 && rArrow.count() && !fTools::equalZero(fWidth)) + { + if(fDockingPosition < 0.0) + { + fDockingPosition = 0.0; + } + else if(fDockingPosition > 1.0) + { + fDockingPosition = 1.0; + } + + // init return value from arrow + aRetval.append(rArrow); + + // get size of the arrow + const B2DRange aArrowSize(getRange(rArrow)); + + // build ArrowTransform; center in X, align with axis in Y + B2DHomMatrix aArrowTransform(basegfx::utils::createTranslateB2DHomMatrix( + -aArrowSize.getCenter().getX(), -aArrowSize.getMinimum().getY())); + + // scale to target size + const double fArrowScale(fWidth / (aArrowSize.getWidth())); + aArrowTransform.scale(fArrowScale, fArrowScale); + + // get arrow size in Y + B2DPoint aUpperCenter(aArrowSize.getCenter().getX(), aArrowSize.getMaximum().getY()); + aUpperCenter *= aArrowTransform; + const double fArrowYLength(B2DVector(aUpperCenter).getLength()); + + // move arrow to have docking position centered + aArrowTransform.translate(0.0, -fArrowYLength * fDockingPosition + fShift); + + // prepare polygon length + if(fTools::equalZero(fCandidateLength)) + { + fCandidateLength = getLength(rCandidate); + } + + // get the polygon vector we want to plant this arrow on + const double fConsumedLength(fArrowYLength * (1.0 - fDockingPosition) - fShift); + const B2DVector aHead(rCandidate.getB2DPoint(bStart ? 0 : rCandidate.count() - 1)); + const B2DVector aTail(getPositionAbsolute(rCandidate, + bStart ? fConsumedLength : fCandidateLength - fConsumedLength, fCandidateLength)); + + // from that vector, take the needed rotation and add rotate for arrow to transformation + const B2DVector aTargetDirection(aHead - aTail); + const double fRotation(atan2(aTargetDirection.getY(), aTargetDirection.getX()) + M_PI_2); + + // rotate around docking position + aArrowTransform.rotate(fRotation); + + // move arrow docking position to polygon head + aArrowTransform.translate(aHead.getX(), aHead.getY()); + + // transform retval and close + aRetval.transform(aArrowTransform); + aRetval.setClosed(true); + + // if pConsumedLength is asked for, fill it + if(pConsumedLength) + { + *pConsumedLength = fConsumedLength; + } + } + + return aRetval; + } +} // end of namespace + +namespace basegfx +{ + // anonymous namespace for local helpers + namespace + { + bool impIsSimpleEdge(const B2DCubicBezier& rCandidate, double fMaxCosQuad, double fMaxPartOfEdgeQuad) + { + // isBezier() is true, already tested by caller + const B2DVector aEdge(rCandidate.getEndPoint() - rCandidate.getStartPoint()); + + if(aEdge.equalZero()) + { + // start and end point the same, but control vectors used -> balloon curve loop + // is not a simple edge + return false; + } + + // get tangentA and scalar with edge + const B2DVector aTangentA(rCandidate.getTangent(0.0)); + const double fScalarAE(aEdge.scalar(aTangentA)); + + if(fTools::lessOrEqual(fScalarAE, 0.0)) + { + // angle between TangentA and Edge is bigger or equal 90 degrees + return false; + } + + // get self-scalars for E and A + const double fScalarE(aEdge.scalar(aEdge)); + const double fScalarA(aTangentA.scalar(aTangentA)); + const double fLengthCompareE(fScalarE * fMaxPartOfEdgeQuad); + + if(fTools::moreOrEqual(fScalarA, fLengthCompareE)) + { + // length of TangentA is more than fMaxPartOfEdge of length of edge + return false; + } + + if(fTools::lessOrEqual(fScalarAE * fScalarAE, fScalarA * fScalarE * fMaxCosQuad)) + { + // angle between TangentA and Edge is bigger or equal angle defined by fMaxCos + return false; + } + + // get tangentB and scalar with edge + const B2DVector aTangentB(rCandidate.getTangent(1.0)); + const double fScalarBE(aEdge.scalar(aTangentB)); + + if(fTools::lessOrEqual(fScalarBE, 0.0)) + { + // angle between TangentB and Edge is bigger or equal 90 degrees + return false; + } + + // get self-scalar for B + const double fScalarB(aTangentB.scalar(aTangentB)); + + if(fTools::moreOrEqual(fScalarB, fLengthCompareE)) + { + // length of TangentB is more than fMaxPartOfEdge of length of edge + return false; + } + + if(fTools::lessOrEqual(fScalarBE * fScalarBE, fScalarB * fScalarE * fMaxCosQuad)) + { + // angle between TangentB and Edge is bigger or equal defined by fMaxCos + return false; + } + + return true; + } + + void impSubdivideToSimple(const B2DCubicBezier& rCandidate, B2DPolygon& rTarget, double fMaxCosQuad, double fMaxPartOfEdgeQuad, sal_uInt32 nMaxRecursionDepth) + { + if(!nMaxRecursionDepth || impIsSimpleEdge(rCandidate, fMaxCosQuad, fMaxPartOfEdgeQuad)) + { + rTarget.appendBezierSegment(rCandidate.getControlPointA(), rCandidate.getControlPointB(), rCandidate.getEndPoint()); + } + else + { + B2DCubicBezier aLeft, aRight; + rCandidate.split(0.5, &aLeft, &aRight); + + impSubdivideToSimple(aLeft, rTarget, fMaxCosQuad, fMaxPartOfEdgeQuad, nMaxRecursionDepth - 1); + impSubdivideToSimple(aRight, rTarget, fMaxCosQuad, fMaxPartOfEdgeQuad, nMaxRecursionDepth - 1); + } + } + + B2DPolygon subdivideToSimple(const B2DPolygon& rCandidate, double fMaxCosQuad, double fMaxPartOfEdgeQuad) + { + const sal_uInt32 nPointCount(rCandidate.count()); + + if(rCandidate.areControlPointsUsed() && nPointCount) + { + const sal_uInt32 nEdgeCount(rCandidate.isClosed() ? nPointCount : nPointCount - 1); + B2DPolygon aRetval; + B2DCubicBezier aEdge; + + // prepare edge for loop + aEdge.setStartPoint(rCandidate.getB2DPoint(0)); + aRetval.append(aEdge.getStartPoint()); + + for(sal_uInt32 a(0); a < nEdgeCount; a++) + { + // fill B2DCubicBezier + const sal_uInt32 nNextIndex((a + 1) % nPointCount); + aEdge.setControlPointA(rCandidate.getNextControlPoint(a)); + aEdge.setControlPointB(rCandidate.getPrevControlPoint(nNextIndex)); + aEdge.setEndPoint(rCandidate.getB2DPoint(nNextIndex)); + + // get rid of unnecessary bezier segments + aEdge.testAndSolveTrivialBezier(); + + if(aEdge.isBezier()) + { + // before splitting recursively with internal simple criteria, use + // ExtremumPosFinder to remove those + std::vector< double > aExtremumPositions; + + aExtremumPositions.reserve(4); + aEdge.getAllExtremumPositions(aExtremumPositions); + + const sal_uInt32 nCount(aExtremumPositions.size()); + + if(nCount) + { + if(nCount > 1) + { + // create order from left to right + std::sort(aExtremumPositions.begin(), aExtremumPositions.end()); + } + + for(sal_uInt32 b(0); b < nCount;) + { + // split aEdge at next split pos + B2DCubicBezier aLeft; + const double fSplitPos(aExtremumPositions[b++]); + + aEdge.split(fSplitPos, &aLeft, &aEdge); + aLeft.testAndSolveTrivialBezier(); + + // consume left part + if(aLeft.isBezier()) + { + impSubdivideToSimple(aLeft, aRetval, fMaxCosQuad, fMaxPartOfEdgeQuad, 6); + } + else + { + aRetval.append(aLeft.getEndPoint()); + } + + if(b < nCount) + { + // correct the remaining split positions to fit to shortened aEdge + const double fScaleFactor(1.0 / (1.0 - fSplitPos)); + + for(sal_uInt32 c(b); c < nCount; c++) + { + aExtremumPositions[c] = (aExtremumPositions[c] - fSplitPos) * fScaleFactor; + } + } + } + + // test the shortened rest of aEdge + aEdge.testAndSolveTrivialBezier(); + + // consume right part + if(aEdge.isBezier()) + { + impSubdivideToSimple(aEdge, aRetval, fMaxCosQuad, fMaxPartOfEdgeQuad, 6); + } + else + { + aRetval.append(aEdge.getEndPoint()); + } + } + else + { + impSubdivideToSimple(aEdge, aRetval, fMaxCosQuad, fMaxPartOfEdgeQuad, 6); + } + } + else + { + // straight edge, add point + aRetval.append(aEdge.getEndPoint()); + } + + // prepare edge for next step + aEdge.setStartPoint(aEdge.getEndPoint()); + } + + // copy closed flag and check for double points + aRetval.setClosed(rCandidate.isClosed()); + aRetval.removeDoublePoints(); + + return aRetval; + } + else + { + return rCandidate; + } + } + + B2DPolygon createAreaGeometryForEdge( + const B2DCubicBezier& rEdge, + double fHalfLineWidth, + bool bStartRound, + bool bEndRound, + bool bStartSquare, + bool bEndSquare, + basegfx::triangulator::B2DTriangleVector* pTriangles) + { + // create polygon for edge + // Unfortunately, while it would be geometrically correct to not add + // the in-between points EdgeEnd and EdgeStart, it leads to rounding + // errors when converting to integer polygon coordinates for painting + if(rEdge.isBezier()) + { + // prepare target and data common for upper and lower + B2DPolygon aBezierPolygon; + const B2DVector aPureEdgeVector(rEdge.getEndPoint() - rEdge.getStartPoint()); + const double fEdgeLength(aPureEdgeVector.getLength()); + const bool bIsEdgeLengthZero(fTools::equalZero(fEdgeLength)); + B2DVector aTangentA(rEdge.getTangent(0.0)); aTangentA.normalize(); + B2DVector aTangentB(rEdge.getTangent(1.0)); aTangentB.normalize(); + const B2DVector aNormalizedPerpendicularA(getPerpendicular(aTangentA)); + const B2DVector aNormalizedPerpendicularB(getPerpendicular(aTangentB)); + + // create upper displacement vectors and check if they cut + const B2DVector aPerpendStartA(aNormalizedPerpendicularA * -fHalfLineWidth); + const B2DVector aPerpendEndA(aNormalizedPerpendicularB * -fHalfLineWidth); + double fCutA(0.0); + const CutFlagValue aCutA(utils::findCut( + rEdge.getStartPoint(), aPerpendStartA, + rEdge.getEndPoint(), aPerpendEndA, + CutFlagValue::ALL, &fCutA)); + const bool bCutA(aCutA != CutFlagValue::NONE); + + // create lower displacement vectors and check if they cut + const B2DVector aPerpendStartB(aNormalizedPerpendicularA * fHalfLineWidth); + const B2DVector aPerpendEndB(aNormalizedPerpendicularB * fHalfLineWidth); + double fCutB(0.0); + const CutFlagValue aCutB(utils::findCut( + rEdge.getEndPoint(), aPerpendEndB, + rEdge.getStartPoint(), aPerpendStartB, + CutFlagValue::ALL, &fCutB)); + const bool bCutB(aCutB != CutFlagValue::NONE); + + // check if cut happens + const bool bCut(bCutA || bCutB); + B2DPoint aCutPoint; + + // create left edge + if(bStartRound || bStartSquare) + { + if(bStartRound) + { + basegfx::B2DPolygon aStartPolygon(utils::createHalfUnitCircle()); + + aStartPolygon.transform( + utils::createScaleShearXRotateTranslateB2DHomMatrix( + fHalfLineWidth, fHalfLineWidth, + 0.0, + atan2(aTangentA.getY(), aTangentA.getX()) + M_PI_2, + rEdge.getStartPoint().getX(), rEdge.getStartPoint().getY())); + aBezierPolygon.append(aStartPolygon); + } + else // bStartSquare + { + const basegfx::B2DPoint aStart(rEdge.getStartPoint() - (aTangentA * fHalfLineWidth)); + + if(bCutB) + { + aBezierPolygon.append(rEdge.getStartPoint() + aPerpendStartB); + } + + aBezierPolygon.append(aStart + aPerpendStartB); + aBezierPolygon.append(aStart + aPerpendStartA); + + if(bCutA) + { + aBezierPolygon.append(rEdge.getStartPoint() + aPerpendStartA); + } + } + } + else + { + // append original in-between point + aBezierPolygon.append(rEdge.getStartPoint()); + } + + // create upper edge. + { + if(bCutA) + { + // calculate cut point and add + aCutPoint = rEdge.getStartPoint() + (aPerpendStartA * fCutA); + aBezierPolygon.append(aCutPoint); + } + else + { + // create scaled bezier segment + const B2DPoint aStart(rEdge.getStartPoint() + aPerpendStartA); + const B2DPoint aEnd(rEdge.getEndPoint() + aPerpendEndA); + const B2DVector aEdge(aEnd - aStart); + const double fLength(aEdge.getLength()); + const double fScale(bIsEdgeLengthZero ? 1.0 : fLength / fEdgeLength); + const B2DVector fRelNext(rEdge.getControlPointA() - rEdge.getStartPoint()); + const B2DVector fRelPrev(rEdge.getControlPointB() - rEdge.getEndPoint()); + + aBezierPolygon.append(aStart); + aBezierPolygon.appendBezierSegment(aStart + (fRelNext * fScale), aEnd + (fRelPrev * fScale), aEnd); + } + } + + // create right edge + if(bEndRound || bEndSquare) + { + if(bEndRound) + { + basegfx::B2DPolygon aEndPolygon(utils::createHalfUnitCircle()); + + aEndPolygon.transform( + utils::createScaleShearXRotateTranslateB2DHomMatrix( + fHalfLineWidth, fHalfLineWidth, + 0.0, + atan2(aTangentB.getY(), aTangentB.getX()) - M_PI_2, + rEdge.getEndPoint().getX(), rEdge.getEndPoint().getY())); + aBezierPolygon.append(aEndPolygon); + } + else // bEndSquare + { + const basegfx::B2DPoint aEnd(rEdge.getEndPoint() + (aTangentB * fHalfLineWidth)); + + if(bCutA) + { + aBezierPolygon.append(rEdge.getEndPoint() + aPerpendEndA); + } + + aBezierPolygon.append(aEnd + aPerpendEndA); + aBezierPolygon.append(aEnd + aPerpendEndB); + + if(bCutB) + { + aBezierPolygon.append(rEdge.getEndPoint() + aPerpendEndB); + } + } + } + else + { + // append original in-between point + aBezierPolygon.append(rEdge.getEndPoint()); + } + + // create lower edge. + { + if(bCutB) + { + // calculate cut point and add + aCutPoint = rEdge.getEndPoint() + (aPerpendEndB * fCutB); + aBezierPolygon.append(aCutPoint); + } + else + { + // create scaled bezier segment + const B2DPoint aStart(rEdge.getEndPoint() + aPerpendEndB); + const B2DPoint aEnd(rEdge.getStartPoint() + aPerpendStartB); + const B2DVector aEdge(aEnd - aStart); + const double fLength(aEdge.getLength()); + const double fScale(bIsEdgeLengthZero ? 1.0 : fLength / fEdgeLength); + const B2DVector fRelNext(rEdge.getControlPointB() - rEdge.getEndPoint()); + const B2DVector fRelPrev(rEdge.getControlPointA() - rEdge.getStartPoint()); + + aBezierPolygon.append(aStart); + aBezierPolygon.appendBezierSegment(aStart + (fRelNext * fScale), aEnd + (fRelPrev * fScale), aEnd); + } + } + + // close + aBezierPolygon.setClosed(true); + + if(bStartRound || bEndRound) + { + // double points possible when round caps are used at start or end + aBezierPolygon.removeDoublePoints(); + } + + if(bCut && ((bStartRound || bStartSquare) && (bEndRound || bEndSquare))) + { + // When cut exists and both ends are extended with caps, a self-intersecting polygon + // is created; one cut point is known, but there is a 2nd one in the caps geometry. + // Solve by using tooling. + // Remark: This nearly never happens due to curve preparations to extreme points + // and maximum angle turning, but I constructed a test case and checked that it is + // working properly. + const B2DPolyPolygon aTemp(utils::solveCrossovers(aBezierPolygon)); + const sal_uInt32 nTempCount(aTemp.count()); + + if(nTempCount) + { + if(nTempCount > 1) + { + // as expected, multiple polygons (with same orientation). Remove + // the one which contains aCutPoint, or better take the one without + for (sal_uInt32 a(0); a < nTempCount; a++) + { + aBezierPolygon = aTemp.getB2DPolygon(a); + + const sal_uInt32 nCandCount(aBezierPolygon.count()); + + for(sal_uInt32 b(0); b < nCandCount; b++) + { + if(aCutPoint.equal(aBezierPolygon.getB2DPoint(b))) + { + aBezierPolygon.clear(); + break; + } + } + + if(aBezierPolygon.count()) + { + break; + } + } + + OSL_ENSURE(aBezierPolygon.count(), "Error in line geometry creation, could not solve self-intersection (!)"); + } + else + { + // none found, use result + aBezierPolygon = aTemp.getB2DPolygon(0); + } + } + else + { + OSL_ENSURE(false, "Error in line geometry creation, could not solve self-intersection (!)"); + } + } + + if(nullptr != pTriangles) + { + const basegfx::triangulator::B2DTriangleVector aResult( + basegfx::triangulator::triangulate( + aBezierPolygon)); + pTriangles->insert(pTriangles->end(), aResult.begin(), aResult.end()); + aBezierPolygon.clear(); + } + + // return + return aBezierPolygon; + } + else + { + // Get start and end point, create tangent and set to needed length + B2DVector aTangent(rEdge.getEndPoint() - rEdge.getStartPoint()); + aTangent.setLength(fHalfLineWidth); + + // prepare return value + B2DPolygon aEdgePolygon; + + // buffered angle + double fAngle(0.0); + bool bAngle(false); + + // buffered perpendicular + B2DVector aPerpend; + bool bPerpend(false); + + // create left vertical + if(bStartRound) + { + aEdgePolygon = utils::createHalfUnitCircle(); + fAngle = atan2(aTangent.getY(), aTangent.getX()); + bAngle = true; + aEdgePolygon.transform( + utils::createScaleShearXRotateTranslateB2DHomMatrix( + fHalfLineWidth, fHalfLineWidth, + 0.0, + fAngle + M_PI_2, + rEdge.getStartPoint().getX(), rEdge.getStartPoint().getY())); + } + else + { + aPerpend.setX(-aTangent.getY()); + aPerpend.setY(aTangent.getX()); + bPerpend = true; + + if(bStartSquare) + { + const basegfx::B2DPoint aStart(rEdge.getStartPoint() - aTangent); + + aEdgePolygon.append(aStart + aPerpend); + aEdgePolygon.append(aStart - aPerpend); + } + else + { + aEdgePolygon.append(rEdge.getStartPoint() + aPerpend); + aEdgePolygon.append(rEdge.getStartPoint()); // keep the in-between point for numerical reasons + aEdgePolygon.append(rEdge.getStartPoint() - aPerpend); + } + } + + // create right vertical + if(bEndRound) + { + basegfx::B2DPolygon aEndPolygon(utils::createHalfUnitCircle()); + + if(!bAngle) + { + fAngle = atan2(aTangent.getY(), aTangent.getX()); + } + + aEndPolygon.transform( + utils::createScaleShearXRotateTranslateB2DHomMatrix( + fHalfLineWidth, fHalfLineWidth, + 0.0, + fAngle - M_PI_2, + rEdge.getEndPoint().getX(), rEdge.getEndPoint().getY())); + aEdgePolygon.append(aEndPolygon); + } + else + { + if(!bPerpend) + { + aPerpend.setX(-aTangent.getY()); + aPerpend.setY(aTangent.getX()); + } + + if(bEndSquare) + { + const basegfx::B2DPoint aEnd(rEdge.getEndPoint() + aTangent); + + aEdgePolygon.append(aEnd - aPerpend); + aEdgePolygon.append(aEnd + aPerpend); + } + else + { + aEdgePolygon.append(rEdge.getEndPoint() - aPerpend); + aEdgePolygon.append(rEdge.getEndPoint()); // keep the in-between point for numerical reasons + aEdgePolygon.append(rEdge.getEndPoint() + aPerpend); + } + } + + // close and return + aEdgePolygon.setClosed(true); + + if(nullptr != pTriangles) + { + const basegfx::triangulator::B2DTriangleVector aResult( + basegfx::triangulator::triangulate( + aEdgePolygon)); + pTriangles->insert(pTriangles->end(), aResult.begin(), aResult.end()); + aEdgePolygon.clear(); + } + + return aEdgePolygon; + } + } + + B2DPolygon createAreaGeometryForJoin( + const B2DVector& rTangentPrev, + const B2DVector& rTangentEdge, + const B2DVector& rPerpendPrev, + const B2DVector& rPerpendEdge, + const B2DPoint& rPoint, + double fHalfLineWidth, + B2DLineJoin eJoin, + double fMiterMinimumAngle) + { + SAL_WARN_IF(fHalfLineWidth <= 0.0,"basegfx","createAreaGeometryForJoin: LineWidth too small (!)"); + assert((eJoin != B2DLineJoin::NONE) && "createAreaGeometryForJoin: B2DLineJoin::NONE not allowed (!)"); + + // LineJoin from tangent rPerpendPrev to tangent rPerpendEdge in rPoint + B2DPolygon aEdgePolygon; + const B2DPoint aStartPoint(rPoint + rPerpendPrev); + const B2DPoint aEndPoint(rPoint + rPerpendEdge); + + // test if for Miter, the angle is too small and the fallback + // to bevel needs to be used + if(eJoin == B2DLineJoin::Miter) + { + const double fAngle(fabs(rPerpendPrev.angle(rPerpendEdge))); + + if((M_PI - fAngle) < fMiterMinimumAngle) + { + // fallback to bevel + eJoin = B2DLineJoin::Bevel; + } + } + + switch(eJoin) + { + case B2DLineJoin::Miter : + { + aEdgePolygon.append(aEndPoint); + aEdgePolygon.append(rPoint); + aEdgePolygon.append(aStartPoint); + + // Look for the cut point between start point along rTangentPrev and + // end point along rTangentEdge. -rTangentEdge should be used, but since + // the cut value is used for interpolating along the first edge, the negation + // is not needed since the same fCut will be found on the first edge. + // If it exists, insert it to complete the mitered fill polygon. + double fCutPos(0.0); + utils::findCut(aStartPoint, rTangentPrev, aEndPoint, rTangentEdge, CutFlagValue::ALL, &fCutPos); + + if(fCutPos != 0.0) + { + const B2DPoint aCutPoint(aStartPoint + (rTangentPrev * fCutPos)); + aEdgePolygon.append(aCutPoint); + } + + break; + } + case B2DLineJoin::Round : + { + // use tooling to add needed EllipseSegment + double fAngleStart(atan2(rPerpendPrev.getY(), rPerpendPrev.getX())); + double fAngleEnd(atan2(rPerpendEdge.getY(), rPerpendEdge.getX())); + + // atan2 results are [-PI .. PI], consolidate to [0.0 .. 2PI] + if(fAngleStart < 0.0) + { + fAngleStart += 2 * M_PI; + } + + if(fAngleEnd < 0.0) + { + fAngleEnd += 2 * M_PI; + } + + const B2DPolygon aBow(utils::createPolygonFromEllipseSegment(rPoint, fHalfLineWidth, fHalfLineWidth, fAngleStart, fAngleEnd)); + + if(aBow.count() > 1) + { + // #i101491# + // use the original start/end positions; the ones from bow creation may be numerically + // different due to their different creation. To guarantee good merging quality with edges + // and edge roundings (and to reduce point count) + aEdgePolygon = aBow; + aEdgePolygon.setB2DPoint(0, aStartPoint); + aEdgePolygon.setB2DPoint(aEdgePolygon.count() - 1, aEndPoint); + aEdgePolygon.append(rPoint); + + break; + } + else + { + [[fallthrough]]; // wanted fall-through to default + } + } + default: // B2DLineJoin::Bevel + { + aEdgePolygon.append(aEndPoint); + aEdgePolygon.append(rPoint); + aEdgePolygon.append(aStartPoint); + + break; + } + } + + // create last polygon part for edge + aEdgePolygon.setClosed(true); + + return aEdgePolygon; + } + } // end of anonymous namespace + + namespace utils + { + B2DPolyPolygon createAreaGeometry( + const B2DPolygon& rCandidate, + double fHalfLineWidth, + B2DLineJoin eJoin, + css::drawing::LineCap eCap, + double fMaxAllowedAngle, + double fMaxPartOfEdge, + double fMiterMinimumAngle) + { + if(fMaxAllowedAngle > M_PI_2) + { + fMaxAllowedAngle = M_PI_2; + } + else if(fMaxAllowedAngle < 0.01 * M_PI_2) + { + fMaxAllowedAngle = 0.01 * M_PI_2; + } + + if(fMaxPartOfEdge > 1.0) + { + fMaxPartOfEdge = 1.0; + } + else if(fMaxPartOfEdge < 0.01) + { + fMaxPartOfEdge = 0.01; + } + + if(fMiterMinimumAngle > M_PI) + { + fMiterMinimumAngle = M_PI; + } + else if(fMiterMinimumAngle < 0.01 * M_PI) + { + fMiterMinimumAngle = 0.01 * M_PI; + } + + B2DPolygon aCandidate(rCandidate); + const double fMaxCos(cos(fMaxAllowedAngle)); + + aCandidate.removeDoublePoints(); + aCandidate = subdivideToSimple(aCandidate, fMaxCos * fMaxCos, fMaxPartOfEdge * fMaxPartOfEdge); + + const sal_uInt32 nPointCount(aCandidate.count()); + + if(nPointCount) + { + B2DPolyPolygon aRetval; + const bool bIsClosed(aCandidate.isClosed()); + const sal_uInt32 nEdgeCount(bIsClosed ? nPointCount : nPointCount - 1); + const bool bLineCap(!bIsClosed && eCap != css::drawing::LineCap_BUTT); + + if(nEdgeCount) + { + B2DCubicBezier aEdge; + B2DCubicBezier aPrev; + + const bool bEventuallyCreateLineJoin(eJoin != B2DLineJoin::NONE); + // prepare edge + aEdge.setStartPoint(aCandidate.getB2DPoint(0)); + + if(bIsClosed && bEventuallyCreateLineJoin) + { + // prepare previous edge + const sal_uInt32 nPrevIndex(nPointCount - 1); + aPrev.setStartPoint(aCandidate.getB2DPoint(nPrevIndex)); + aPrev.setControlPointA(aCandidate.getNextControlPoint(nPrevIndex)); + aPrev.setControlPointB(aCandidate.getPrevControlPoint(0)); + aPrev.setEndPoint(aEdge.getStartPoint()); + } + + for(sal_uInt32 a(0); a < nEdgeCount; a++) + { + // fill current Edge + const sal_uInt32 nNextIndex((a + 1) % nPointCount); + aEdge.setControlPointA(aCandidate.getNextControlPoint(a)); + aEdge.setControlPointB(aCandidate.getPrevControlPoint(nNextIndex)); + aEdge.setEndPoint(aCandidate.getB2DPoint(nNextIndex)); + + // check and create linejoin + if(bEventuallyCreateLineJoin && (bIsClosed || a != 0)) + { + B2DVector aTangentPrev(aPrev.getTangent(1.0)); aTangentPrev.normalize(); + B2DVector aTangentEdge(aEdge.getTangent(0.0)); aTangentEdge.normalize(); + B2VectorOrientation aOrientation(getOrientation(aTangentPrev, aTangentEdge)); + + if(aOrientation == B2VectorOrientation::Neutral) + { + // they are parallel or empty; if they are both not zero and point + // in opposite direction, a half-circle is needed + if(!aTangentPrev.equalZero() && !aTangentEdge.equalZero()) + { + const double fAngle(fabs(aTangentPrev.angle(aTangentEdge))); + + if(fTools::equal(fAngle, M_PI)) + { + // for half-circle production, fallback to positive + // orientation + aOrientation = B2VectorOrientation::Positive; + } + } + } + + if(aOrientation == B2VectorOrientation::Positive) + { + const B2DVector aPerpendPrev(getPerpendicular(aTangentPrev) * -fHalfLineWidth); + const B2DVector aPerpendEdge(getPerpendicular(aTangentEdge) * -fHalfLineWidth); + + aRetval.append( + createAreaGeometryForJoin( + aTangentPrev, + aTangentEdge, + aPerpendPrev, + aPerpendEdge, + aEdge.getStartPoint(), + fHalfLineWidth, + eJoin, + fMiterMinimumAngle)); + } + else if(aOrientation == B2VectorOrientation::Negative) + { + const B2DVector aPerpendPrev(getPerpendicular(aTangentPrev) * fHalfLineWidth); + const B2DVector aPerpendEdge(getPerpendicular(aTangentEdge) * fHalfLineWidth); + + aRetval.append( + createAreaGeometryForJoin( + aTangentEdge, + aTangentPrev, + aPerpendEdge, + aPerpendPrev, + aEdge.getStartPoint(), + fHalfLineWidth, + eJoin, + fMiterMinimumAngle)); + } + } + + // create geometry for edge + const bool bLast(a + 1 == nEdgeCount); + + if(bLineCap) + { + const bool bFirst(!a); + + aRetval.append( + createAreaGeometryForEdge( + aEdge, + fHalfLineWidth, + bFirst && eCap == css::drawing::LineCap_ROUND, + bLast && eCap == css::drawing::LineCap_ROUND, + bFirst && eCap == css::drawing::LineCap_SQUARE, + bLast && eCap == css::drawing::LineCap_SQUARE, + nullptr)); + } + else + { + aRetval.append( + createAreaGeometryForEdge( + aEdge, + fHalfLineWidth, + false, + false, + false, + false, + nullptr)); + } + + // prepare next step + if(!bLast) + { + if(bEventuallyCreateLineJoin) + { + aPrev = aEdge; + } + + aEdge.setStartPoint(aEdge.getEndPoint()); + } + } + } + else + { + // point count, but no edge count -> single point + const basegfx::B2DPolygon aCircle( + createPolygonFromCircle( + aCandidate.getB2DPoint(0), + fHalfLineWidth)); + + aRetval.append(aCircle); + } + + return aRetval; + } + else + { + return B2DPolyPolygon(rCandidate); + } + } + } // end of namespace utils +} // end of namespace basegfx + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/basegfx/source/polygon/b2dpolygon.cxx b/basegfx/source/polygon/b2dpolygon.cxx new file mode 100644 index 0000000000..cf7309d20d --- /dev/null +++ b/basegfx/source/polygon/b2dpolygon.cxx @@ -0,0 +1,1455 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * 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 . + */ + +#include <sal/config.h> + +#include <basegfx/polygon/b2dpolygon.hxx> +#include <basegfx/point/b2dpoint.hxx> +#include <basegfx/vector/b2dvector.hxx> +#include <basegfx/matrix/b2dhommatrix.hxx> +#include <basegfx/curve/b2dcubicbezier.hxx> +#include <basegfx/polygon/b2dpolygontools.hxx> +#include <basegfx/utils/systemdependentdata.hxx> +#include <memory> +#include <vector> + +namespace +{ + +class CoordinateDataArray2D +{ +private: + std::vector<basegfx::B2DPoint> maVector; + +public: + explicit CoordinateDataArray2D(sal_uInt32 nCount) + : maVector(nCount) + { + } + + CoordinateDataArray2D(const CoordinateDataArray2D& rOriginal, sal_uInt32 nIndex, sal_uInt32 nCount) + : maVector(rOriginal.maVector.begin() + nIndex, rOriginal.maVector.begin() + (nIndex + nCount)) + { + } + + sal_uInt32 count() const + { + return maVector.size(); + } + + bool operator==(const CoordinateDataArray2D& rCandidate) const + { + return (maVector == rCandidate.maVector); + } + + const basegfx::B2DPoint& getCoordinate(sal_uInt32 nIndex) const + { + assert(nIndex < maVector.size()); + return maVector[nIndex]; + } + + void setCoordinate(sal_uInt32 nIndex, const basegfx::B2DPoint& rValue) + { + assert(nIndex < maVector.size()); + maVector[nIndex] = rValue; + } + + void reserve(sal_uInt32 nCount) + { + maVector.reserve(nCount); + } + + void append(const basegfx::B2DPoint& rValue) + { + maVector.push_back(rValue); + } + + void insert(sal_uInt32 nIndex, const basegfx::B2DPoint& rValue, sal_uInt32 nCount) + { + assert(nCount > 0); + assert(nIndex <= maVector.size()); + // add nCount copies of rValue + maVector.insert(maVector.begin() + nIndex, nCount, rValue); + } + + void insert(sal_uInt32 nIndex, const CoordinateDataArray2D& rSource) + { + assert(rSource.maVector.size() > 0); + assert(nIndex <= maVector.size()); + // insert data + auto aIndex = maVector.begin(); + aIndex += nIndex; + auto aStart = rSource.maVector.cbegin(); + auto aEnd = rSource.maVector.cend(); + maVector.insert(aIndex, aStart, aEnd); + } + + void remove(sal_uInt32 nIndex, sal_uInt32 nCount) + { + assert(nCount > 0); + assert(nIndex + nCount <= maVector.size()); + // remove point data + const auto aStart = maVector.begin() + nIndex; + const auto aEnd = aStart + nCount; + maVector.erase(aStart, aEnd); + } + + void flip(bool bIsClosed) + { + assert(maVector.size() > 1); + + // to keep the same point at index 0, just flip all points except the + // first one when closed + const sal_uInt32 nHalfSize(bIsClosed ? (maVector.size() - 1) >> 1 : maVector.size() >> 1); + auto aStart = bIsClosed ? maVector.begin() + 1 : maVector.begin(); + auto aEnd = maVector.end() - 1; + + for(sal_uInt32 a(0); a < nHalfSize; a++) + { + std::swap(*aStart, *aEnd); + ++aStart; + --aEnd; + } + } + + void removeDoublePointsAtBeginEnd() + { + // remove from end as long as there are at least two points + // and begin/end are equal + while((maVector.size() > 1) && (maVector[0] == maVector[maVector.size() - 1])) + { + maVector.pop_back(); + } + } + + void removeDoublePointsWholeTrack() + { + sal_uInt32 nIndex(0); + + // test as long as there are at least two points and as long as the index + // is smaller or equal second last point + while((maVector.size() > 1) && (nIndex <= maVector.size() - 2)) + { + if(maVector[nIndex] == maVector[nIndex + 1]) + { + // if next is same as index, delete next + maVector.erase(maVector.begin() + (nIndex + 1)); + } + else + { + // if different, step forward + nIndex++; + } + } + } + + void transform(const basegfx::B2DHomMatrix& rMatrix) + { + for (auto& point : maVector) + { + point *= rMatrix; + } + } +}; + +class ControlVectorPair2D +{ + basegfx::B2DVector maPrevVector; + basegfx::B2DVector maNextVector; + +public: + explicit ControlVectorPair2D() {} + + const basegfx::B2DVector& getPrevVector() const + { + return maPrevVector; + } + + void setPrevVector(const basegfx::B2DVector& rValue) + { + if(rValue != maPrevVector) + maPrevVector = rValue; + } + + const basegfx::B2DVector& getNextVector() const + { + return maNextVector; + } + + void setNextVector(const basegfx::B2DVector& rValue) + { + if(rValue != maNextVector) + maNextVector = rValue; + } + + bool operator==(const ControlVectorPair2D& rData) const + { + return (maPrevVector == rData.getPrevVector() && maNextVector == rData.getNextVector()); + } + + void flip() + { + std::swap(maPrevVector, maNextVector); + } +}; + +class ControlVectorArray2D +{ + typedef std::vector< ControlVectorPair2D > ControlVectorPair2DVector; + + ControlVectorPair2DVector maVector; + sal_uInt32 mnUsedVectors; + +public: + explicit ControlVectorArray2D(sal_uInt32 nCount) + : maVector(nCount), + mnUsedVectors(0) + {} + + ControlVectorArray2D(const ControlVectorArray2D& rOriginal, sal_uInt32 nIndex, sal_uInt32 nCount) + : mnUsedVectors(0) + { + assert(nIndex + nCount <= rOriginal.maVector.size()); + auto aStart(rOriginal.maVector.begin() + nIndex); + auto aEnd(aStart + nCount); + maVector.reserve(nCount); + + for(; aStart != aEnd; ++aStart) + { + if(!aStart->getPrevVector().equalZero()) + mnUsedVectors++; + + if(!aStart->getNextVector().equalZero()) + mnUsedVectors++; + + maVector.push_back(*aStart); + } + } + + bool operator==(const ControlVectorArray2D& rCandidate) const + { + return (maVector == rCandidate.maVector); + } + + bool isUsed() const + { + return (mnUsedVectors != 0); + } + + const basegfx::B2DVector& getPrevVector(sal_uInt32 nIndex) const + { + assert(nIndex < maVector.size()); + return maVector[nIndex].getPrevVector(); + } + + void setPrevVector(sal_uInt32 nIndex, const basegfx::B2DVector& rValue) + { + bool bWasUsed(mnUsedVectors && !maVector[nIndex].getPrevVector().equalZero()); + bool bIsUsed(!rValue.equalZero()); + + if(bWasUsed) + { + if(bIsUsed) + { + maVector[nIndex].setPrevVector(rValue); + } + else + { + maVector[nIndex].setPrevVector(basegfx::B2DVector::getEmptyVector()); + mnUsedVectors--; + } + } + else + { + if(bIsUsed) + { + maVector[nIndex].setPrevVector(rValue); + mnUsedVectors++; + } + } + } + + const basegfx::B2DVector& getNextVector(sal_uInt32 nIndex) const + { + assert(nIndex < maVector.size()); + return maVector[nIndex].getNextVector(); + } + + void setNextVector(sal_uInt32 nIndex, const basegfx::B2DVector& rValue) + { + bool bWasUsed(mnUsedVectors && !maVector[nIndex].getNextVector().equalZero()); + bool bIsUsed(!rValue.equalZero()); + + if(bWasUsed) + { + if(bIsUsed) + { + maVector[nIndex].setNextVector(rValue); + } + else + { + maVector[nIndex].setNextVector(basegfx::B2DVector::getEmptyVector()); + mnUsedVectors--; + } + } + else + { + if(bIsUsed) + { + maVector[nIndex].setNextVector(rValue); + mnUsedVectors++; + } + } + } + + void append(const ControlVectorPair2D& rValue) + { + maVector.push_back(rValue); + + if(!rValue.getPrevVector().equalZero()) + mnUsedVectors += 1; + + if(!rValue.getNextVector().equalZero()) + mnUsedVectors += 1; + } + + void insert(sal_uInt32 nIndex, const ControlVectorPair2D& rValue, sal_uInt32 nCount) + { + assert(nCount > 0); + assert(nIndex <= maVector.size()); + + // add nCount copies of rValue + maVector.insert(maVector.begin() + nIndex, nCount, rValue); + + if(!rValue.getPrevVector().equalZero()) + mnUsedVectors += nCount; + + if(!rValue.getNextVector().equalZero()) + mnUsedVectors += nCount; + } + + void insert(sal_uInt32 nIndex, const ControlVectorArray2D& rSource) + { + assert(rSource.maVector.size() > 0); + assert(nIndex <= maVector.size()); + + // insert data + ControlVectorPair2DVector::iterator aIndex(maVector.begin() + nIndex); + ControlVectorPair2DVector::const_iterator aStart(rSource.maVector.begin()); + ControlVectorPair2DVector::const_iterator aEnd(rSource.maVector.end()); + maVector.insert(aIndex, aStart, aEnd); + + for(; aStart != aEnd; ++aStart) + { + if(!aStart->getPrevVector().equalZero()) + mnUsedVectors++; + + if(!aStart->getNextVector().equalZero()) + mnUsedVectors++; + } + } + + void remove(sal_uInt32 nIndex, sal_uInt32 nCount) + { + assert(nCount > 0); + assert(nIndex + nCount <= maVector.size()); + + const ControlVectorPair2DVector::iterator aDeleteStart(maVector.begin() + nIndex); + const ControlVectorPair2DVector::iterator aDeleteEnd(aDeleteStart + nCount); + ControlVectorPair2DVector::const_iterator aStart(aDeleteStart); + + for(; mnUsedVectors && aStart != aDeleteEnd; ++aStart) + { + if(!aStart->getPrevVector().equalZero()) + mnUsedVectors--; + + if(mnUsedVectors && !aStart->getNextVector().equalZero()) + mnUsedVectors--; + } + + // remove point data + maVector.erase(aDeleteStart, aDeleteEnd); + } + + void flip(bool bIsClosed) + { + assert(maVector.size() > 1); + + // to keep the same point at index 0, just flip all points except the + // first one when closed + const sal_uInt32 nHalfSize(bIsClosed ? (maVector.size() - 1) >> 1 : maVector.size() >> 1); + ControlVectorPair2DVector::iterator aStart(bIsClosed ? maVector.begin() + 1 : maVector.begin()); + ControlVectorPair2DVector::iterator aEnd(maVector.end() - 1); + + for(sal_uInt32 a(0); a < nHalfSize; a++) + { + // swap Prev and Next + aStart->flip(); + aEnd->flip(); + + // swap entries + std::swap(*aStart, *aEnd); + + ++aStart; + --aEnd; + } + + if(aStart == aEnd) + { + // swap Prev and Next at middle element (if exists) + aStart->flip(); + } + + if(bIsClosed) + { + // swap Prev and Next at start element + maVector.begin()->flip(); + } + } +}; + +class ImplBufferedData : public basegfx::SystemDependentDataHolder +{ +private: + // Possibility to hold the last subdivision + mutable std::optional< basegfx::B2DPolygon > mpDefaultSubdivision; + + // Possibility to hold the last B2DRange calculation + mutable std::optional< basegfx::B2DRange > moB2DRange; + +public: + ImplBufferedData() + { + } + + const basegfx::B2DPolygon& getDefaultAdaptiveSubdivision(const basegfx::B2DPolygon& rSource) const + { + if(!mpDefaultSubdivision) + { + mpDefaultSubdivision = basegfx::utils::adaptiveSubdivideByAngle(rSource); + } + + return *mpDefaultSubdivision; + } + + const basegfx::B2DRange& getB2DRange(const basegfx::B2DPolygon& rSource) const + { + if(!moB2DRange) + { + basegfx::B2DRange aNewRange; + const sal_uInt32 nPointCount(rSource.count()); + + if(nPointCount) + { + for(sal_uInt32 a(0); a < nPointCount; a++) + { + aNewRange.expand(rSource.getB2DPoint(a)); + } + + if(rSource.areControlPointsUsed()) + { + const sal_uInt32 nEdgeCount(rSource.isClosed() ? nPointCount : nPointCount - 1); + + if(nEdgeCount) + { + basegfx::B2DCubicBezier aEdge; + aEdge.setStartPoint(rSource.getB2DPoint(0)); + + for(sal_uInt32 b(0); b < nEdgeCount; b++) + { + const sal_uInt32 nNextIndex((b + 1) % nPointCount); + aEdge.setControlPointA(rSource.getNextControlPoint(b)); + aEdge.setControlPointB(rSource.getPrevControlPoint(nNextIndex)); + aEdge.setEndPoint(rSource.getB2DPoint(nNextIndex)); + + if(aEdge.isBezier()) + { + const basegfx::B2DRange aBezierRangeWithControlPoints(aEdge.getRange()); + + if(!aNewRange.isInside(aBezierRangeWithControlPoints)) + { + // the range with control points of the current edge is not completely + // inside the current range without control points. Expand current range by + // subdividing the bezier segment. + // Ideal here is a subdivision at the extreme values, so use + // getAllExtremumPositions to get all extremas in one run + std::vector< double > aExtremas; + + aExtremas.reserve(4); + aEdge.getAllExtremumPositions(aExtremas); + + const sal_uInt32 nExtremaCount(aExtremas.size()); + + for(sal_uInt32 c(0); c < nExtremaCount; c++) + { + aNewRange.expand(aEdge.interpolatePoint(aExtremas[c])); + } + } + } + + // prepare next edge + aEdge.setStartPoint(aEdge.getEndPoint()); + } + } + } + } + + moB2DRange = aNewRange; + } + + return *moB2DRange; + } +}; + +} + +class ImplB2DPolygon +{ +private: + // The point vector. This vector exists always and defines the + // count of members. + CoordinateDataArray2D maPoints; + + // The control point vectors. This vectors are created on demand + // and may be zero. + std::optional< ControlVectorArray2D > moControlVector; + + // buffered data for e.g. default subdivision and range + // we do not want to 'modify' the ImplB2DPolygon, + // but add buffered data that is valid for all referencing instances + mutable std::unique_ptr<ImplBufferedData> mpBufferedData; + + // flag which decides if this polygon is opened or closed + bool mbIsClosed; + +public: + const basegfx::B2DPolygon& getDefaultAdaptiveSubdivision(const basegfx::B2DPolygon& rSource) const + { + if(!moControlVector || !moControlVector->isUsed()) + { + return rSource; + } + + if(!mpBufferedData) + { + mpBufferedData.reset(new ImplBufferedData); + } + + return mpBufferedData->getDefaultAdaptiveSubdivision(rSource); + } + + const basegfx::B2DRange& getB2DRange(const basegfx::B2DPolygon& rSource) const + { + if(!mpBufferedData) + { + mpBufferedData.reset(new ImplBufferedData); + } + + return mpBufferedData->getB2DRange(rSource); + } + + ImplB2DPolygon() + : maPoints(0), + mbIsClosed(false) + {} + + ImplB2DPolygon(const ImplB2DPolygon& rToBeCopied) + : maPoints(rToBeCopied.maPoints), + mbIsClosed(rToBeCopied.mbIsClosed) + { + // complete initialization using copy + if(rToBeCopied.moControlVector && rToBeCopied.moControlVector->isUsed()) + { + moControlVector.emplace( *rToBeCopied.moControlVector ); + } + } + + ImplB2DPolygon(const ImplB2DPolygon& rToBeCopied, sal_uInt32 nIndex, sal_uInt32 nCount) + : maPoints(rToBeCopied.maPoints, nIndex, nCount), + mbIsClosed(rToBeCopied.mbIsClosed) + { + // complete initialization using partly copy + if(rToBeCopied.moControlVector && rToBeCopied.moControlVector->isUsed()) + { + moControlVector.emplace( *rToBeCopied.moControlVector, nIndex, nCount ); + + if(!moControlVector->isUsed()) + moControlVector.reset(); + } + } + + ImplB2DPolygon& operator=(const ImplB2DPolygon& rOther) + { + if (this != &rOther) + { + moControlVector.reset(); + mpBufferedData.reset(); + maPoints = rOther.maPoints; + mbIsClosed = rOther.mbIsClosed; + if (rOther.moControlVector && rOther.moControlVector->isUsed()) + { + moControlVector.emplace( *rOther.moControlVector ); + + if(!moControlVector->isUsed()) + moControlVector.reset(); + } + } + return *this; + } + + sal_uInt32 count() const + { + return maPoints.count(); + } + + bool isClosed() const + { + return mbIsClosed; + } + + void setClosed(bool bNew) + { + if(bNew != mbIsClosed) + { + mpBufferedData.reset(); + mbIsClosed = bNew; + } + } + + bool operator==(const ImplB2DPolygon& rCandidate) const + { + if(mbIsClosed != rCandidate.mbIsClosed) + return false; + if(!(maPoints == rCandidate.maPoints)) + return false; + bool bControlVectorsAreEqual(true); + + if(moControlVector) + { + if(rCandidate.moControlVector) + { + bControlVectorsAreEqual = ((*moControlVector) == (*rCandidate.moControlVector)); + } + else + { + // candidate has no control vector, so it's assumed all unused. + bControlVectorsAreEqual = !moControlVector->isUsed(); + } + } + else + { + if(rCandidate.moControlVector) + { + // we have no control vector, so it's assumed all unused. + bControlVectorsAreEqual = !rCandidate.moControlVector->isUsed(); + } + } + + return bControlVectorsAreEqual; + } + + const basegfx::B2DPoint& getPoint(sal_uInt32 nIndex) const + { + return maPoints.getCoordinate(nIndex); + } + + void setPoint(sal_uInt32 nIndex, const basegfx::B2DPoint& rValue) + { + mpBufferedData.reset(); + maPoints.setCoordinate(nIndex, rValue); + } + + void reserve(sal_uInt32 nCount) + { + maPoints.reserve(nCount); + } + + void append(const basegfx::B2DPoint& rPoint) + { + mpBufferedData.reset(); // TODO: is this needed? + const auto aCoordinate = rPoint; + maPoints.append(aCoordinate); + + if(moControlVector) + { + const ControlVectorPair2D aVectorPair; + moControlVector->append(aVectorPair); + } + } + + void insert(sal_uInt32 nIndex, const basegfx::B2DPoint& rPoint, sal_uInt32 nCount) + { + assert(nCount > 0); + mpBufferedData.reset(); + auto aCoordinate = rPoint; + maPoints.insert(nIndex, aCoordinate, nCount); + + if(moControlVector) + { + ControlVectorPair2D aVectorPair; + moControlVector->insert(nIndex, aVectorPair, nCount); + } + } + + void append(const basegfx::B2DPoint& rPoint, sal_uInt32 nCount) + { + insert(count(), rPoint, nCount); + } + + const basegfx::B2DVector& getPrevControlVector(sal_uInt32 nIndex) const + { + if(moControlVector) + { + return moControlVector->getPrevVector(nIndex); + } + else + { + return basegfx::B2DVector::getEmptyVector(); + } + } + + void setPrevControlVector(sal_uInt32 nIndex, const basegfx::B2DVector& rValue) + { + if(!moControlVector) + { + if(!rValue.equalZero()) + { + mpBufferedData.reset(); + moControlVector.emplace(maPoints.count()); + moControlVector->setPrevVector(nIndex, rValue); + } + } + else + { + mpBufferedData.reset(); + moControlVector->setPrevVector(nIndex, rValue); + + if(!moControlVector->isUsed()) + moControlVector.reset(); + } + } + + const basegfx::B2DVector& getNextControlVector(sal_uInt32 nIndex) const + { + if(moControlVector) + { + return moControlVector->getNextVector(nIndex); + } + else + { + return basegfx::B2DVector::getEmptyVector(); + } + } + + void setNextControlVector(sal_uInt32 nIndex, const basegfx::B2DVector& rValue) + { + if(!moControlVector) + { + if(!rValue.equalZero()) + { + mpBufferedData.reset(); + moControlVector.emplace(maPoints.count()); + moControlVector->setNextVector(nIndex, rValue); + } + } + else + { + mpBufferedData.reset(); + moControlVector->setNextVector(nIndex, rValue); + + if(!moControlVector->isUsed()) + moControlVector.reset(); + } + } + + bool areControlPointsUsed() const + { + return (moControlVector && moControlVector->isUsed()); + } + + void resetControlVectors() + { + mpBufferedData.reset(); + moControlVector.reset(); + } + + void setControlVectors(sal_uInt32 nIndex, const basegfx::B2DVector& rPrev, const basegfx::B2DVector& rNext) + { + setPrevControlVector(nIndex, rPrev); + setNextControlVector(nIndex, rNext); + } + + void appendBezierSegment(const basegfx::B2DVector& rNext, const basegfx::B2DVector& rPrev, const basegfx::B2DPoint& rPoint) + { + mpBufferedData.reset(); + const sal_uInt32 nCount(maPoints.count()); + + if(nCount) + { + setNextControlVector(nCount - 1, rNext); + } + + insert(nCount, rPoint, 1); + setPrevControlVector(nCount, rPrev); + } + + void append(const ImplB2DPolygon& rSource) + { + assert(rSource.maPoints.count() > 0); + const sal_uInt32 nIndex = count(); + + mpBufferedData.reset(); + + maPoints.insert(nIndex, rSource.maPoints); + + if(rSource.moControlVector) + { + if (!moControlVector) + moControlVector.emplace(nIndex); + moControlVector->insert(nIndex, *rSource.moControlVector); + + if(!moControlVector->isUsed()) + moControlVector.reset(); + } + else if(moControlVector) + { + ControlVectorPair2D aVectorPair; + moControlVector->insert(nIndex, aVectorPair, rSource.count()); + } + } + + void remove(sal_uInt32 nIndex, sal_uInt32 nCount) + { + assert(nCount > 0); + mpBufferedData.reset(); + maPoints.remove(nIndex, nCount); + + if(moControlVector) + { + moControlVector->remove(nIndex, nCount); + + if(!moControlVector->isUsed()) + moControlVector.reset(); + } + } + + void flip() + { + assert(maPoints.count() > 1); + + mpBufferedData.reset(); + + // flip points + maPoints.flip(mbIsClosed); + + if(moControlVector) + { + // flip control vector + moControlVector->flip(mbIsClosed); + } + } + + bool hasDoublePoints() const + { + if(mbIsClosed) + { + // check for same start and end point + const sal_uInt32 nIndex(maPoints.count() - 1); + + if(maPoints.getCoordinate(0) == maPoints.getCoordinate(nIndex)) + { + if(moControlVector) + { + if(moControlVector->getNextVector(nIndex).equalZero() && moControlVector->getPrevVector(0).equalZero()) + { + return true; + } + } + else + { + return true; + } + } + } + + // test for range + for(sal_uInt32 a(0); a < maPoints.count() - 1; a++) + { + if(maPoints.getCoordinate(a) == maPoints.getCoordinate(a + 1)) + { + if(moControlVector) + { + if(moControlVector->getNextVector(a).equalZero() && moControlVector->getPrevVector(a + 1).equalZero()) + { + return true; + } + } + else + { + return true; + } + } + } + + return false; + } + + void removeDoublePointsAtBeginEnd() + { + // Only remove DoublePoints at Begin and End when poly is closed + if(!mbIsClosed) + return; + + mpBufferedData.reset(); + + if(moControlVector) + { + bool bRemove; + + do + { + bRemove = false; + + if(maPoints.count() > 1) + { + const sal_uInt32 nIndex(maPoints.count() - 1); + + if(maPoints.getCoordinate(0) == maPoints.getCoordinate(nIndex)) + { + if(moControlVector) + { + if(moControlVector->getNextVector(nIndex).equalZero() && moControlVector->getPrevVector(0).equalZero()) + { + bRemove = true; + } + } + else + { + bRemove = true; + } + } + } + + if(bRemove) + { + const sal_uInt32 nIndex(maPoints.count() - 1); + + if(moControlVector && !moControlVector->getPrevVector(nIndex).equalZero()) + { + moControlVector->setPrevVector(0, moControlVector->getPrevVector(nIndex)); + } + + remove(nIndex, 1); + } + } + while(bRemove); + } + else + { + maPoints.removeDoublePointsAtBeginEnd(); + } + } + + void removeDoublePointsWholeTrack() + { + mpBufferedData.reset(); + + if(moControlVector) + { + sal_uInt32 nIndex(0); + + // test as long as there are at least two points and as long as the index + // is smaller or equal second last point + while((maPoints.count() > 1) && (nIndex <= maPoints.count() - 2)) + { + bool bRemove(maPoints.getCoordinate(nIndex) == maPoints.getCoordinate(nIndex + 1)); + + if(bRemove && moControlVector) + { + if(!moControlVector->getNextVector(nIndex).equalZero() || !moControlVector->getPrevVector(nIndex + 1).equalZero()) + { + bRemove = false; + } + } + + if(bRemove) + { + if(moControlVector && !moControlVector->getPrevVector(nIndex).equalZero()) + { + moControlVector->setPrevVector(nIndex + 1, moControlVector->getPrevVector(nIndex)); + } + + // if next is same as index and the control vectors are unused, delete index + remove(nIndex, 1); + } + else + { + // if different, step forward + nIndex++; + } + } + } + else + { + maPoints.removeDoublePointsWholeTrack(); + } + } + + void transform(const basegfx::B2DHomMatrix& rMatrix) + { + mpBufferedData.reset(); + + if(moControlVector) + { + for(sal_uInt32 a(0); a < maPoints.count(); a++) + { + basegfx::B2DPoint aCandidate = maPoints.getCoordinate(a); + + if(moControlVector->isUsed()) + { + const basegfx::B2DVector& rPrevVector(moControlVector->getPrevVector(a)); + const basegfx::B2DVector& rNextVector(moControlVector->getNextVector(a)); + + if(!rPrevVector.equalZero()) + { + basegfx::B2DVector aPrevVector(rMatrix * rPrevVector); + moControlVector->setPrevVector(a, aPrevVector); + } + + if(!rNextVector.equalZero()) + { + basegfx::B2DVector aNextVector(rMatrix * rNextVector); + moControlVector->setNextVector(a, aNextVector); + } + } + + aCandidate *= rMatrix; + maPoints.setCoordinate(a, aCandidate); + } + + if(!moControlVector->isUsed()) + moControlVector.reset(); + } + else + { + maPoints.transform(rMatrix); + } + } + + void addOrReplaceSystemDependentData(basegfx::SystemDependentData_SharedPtr& rData) const + { + if(!mpBufferedData) + { + mpBufferedData.reset(new ImplBufferedData); + } + + mpBufferedData->addOrReplaceSystemDependentData(rData); + } + + basegfx::SystemDependentData_SharedPtr getSystemDependentData(size_t hash_code) const + { + if(mpBufferedData) + { + return mpBufferedData->getSystemDependentData(hash_code); + } + + return basegfx::SystemDependentData_SharedPtr(); + } +}; + +namespace basegfx +{ + static o3tl::cow_wrapper<ImplB2DPolygon> DEFAULT; + + B2DPolygon::B2DPolygon() + : mpPolygon(DEFAULT) {} + + B2DPolygon::B2DPolygon(std::initializer_list<basegfx::B2DPoint> aPoints) + { + for (const basegfx::B2DPoint& rPoint : aPoints) + { + append(rPoint); + } + } + + B2DPolygon::B2DPolygon(const B2DPolygon&) = default; + + B2DPolygon::B2DPolygon(B2DPolygon&&) = default; + + B2DPolygon::B2DPolygon(const B2DPolygon& rPolygon, sal_uInt32 nIndex, sal_uInt32 nCount) + : mpPolygon(ImplB2DPolygon(*rPolygon.mpPolygon, nIndex, nCount)) + { + } + + B2DPolygon::~B2DPolygon() = default; + + B2DPolygon& B2DPolygon::operator=(const B2DPolygon&) = default; + + B2DPolygon& B2DPolygon::operator=(B2DPolygon&&) = default; + + void B2DPolygon::makeUnique() + { + mpPolygon.make_unique(); + } + + bool B2DPolygon::operator==(const B2DPolygon& rPolygon) const + { + if(mpPolygon.same_object(rPolygon.mpPolygon)) + return true; + + return ((*mpPolygon) == (*rPolygon.mpPolygon)); + } + + sal_uInt32 B2DPolygon::count() const + { + return mpPolygon->count(); + } + + B2DPoint const & B2DPolygon::getB2DPoint(sal_uInt32 nIndex) const + { + return mpPolygon->getPoint(nIndex); + } + + void B2DPolygon::setB2DPoint(sal_uInt32 nIndex, const B2DPoint& rValue) + { + if(getB2DPoint(nIndex) != rValue) + { + mpPolygon->setPoint(nIndex, rValue); + } + } + + void B2DPolygon::reserve(sal_uInt32 nCount) + { + mpPolygon->reserve(nCount); + } + + void B2DPolygon::insert(sal_uInt32 nIndex, const B2DPoint& rPoint, sal_uInt32 nCount) + { + if(nCount) + { + mpPolygon->insert(nIndex, rPoint, nCount); + } + } + + void B2DPolygon::append(const B2DPoint& rPoint, sal_uInt32 nCount) + { + if(nCount) + { + mpPolygon->append(rPoint, nCount); + } + } + + void B2DPolygon::append(const B2DPoint& rPoint) + { + mpPolygon->append(rPoint); + } + + const basegfx::B2DVector& B2DPolygon::getPrevControlVector(sal_uInt32 nIndex) const + { + return mpPolygon->getPrevControlVector(nIndex); + } + + const basegfx::B2DVector& B2DPolygon::getNextControlVector(sal_uInt32 nIndex) const + { + return mpPolygon->getNextControlVector(nIndex); + } + + B2DPoint B2DPolygon::getPrevControlPoint(sal_uInt32 nIndex) const + { + if(areControlPointsUsed()) + { + return getB2DPoint(nIndex) + getPrevControlVector(nIndex); + } + else + { + return getB2DPoint(nIndex); + } + } + + B2DPoint B2DPolygon::getNextControlPoint(sal_uInt32 nIndex) const + { + if(areControlPointsUsed()) + { + return getB2DPoint(nIndex) + getNextControlVector(nIndex); + } + else + { + return getB2DPoint(nIndex); + } + } + + void B2DPolygon::setPrevControlPoint(sal_uInt32 nIndex, const B2DPoint& rValue) + { + const basegfx::B2DVector aNewVector(rValue - getB2DPoint(nIndex)); + + if(getPrevControlVector(nIndex) != aNewVector) + { + mpPolygon->setPrevControlVector(nIndex, aNewVector); + } + } + + void B2DPolygon::setNextControlPoint(sal_uInt32 nIndex, const B2DPoint& rValue) + { + const basegfx::B2DVector aNewVector(rValue - getB2DPoint(nIndex)); + + if(getNextControlVector(nIndex) != aNewVector) + { + mpPolygon->setNextControlVector(nIndex, aNewVector); + } + } + + void B2DPolygon::setControlPoints(sal_uInt32 nIndex, const basegfx::B2DPoint& rPrev, const basegfx::B2DPoint& rNext) + { + const B2DPoint aPoint(getB2DPoint(nIndex)); + const basegfx::B2DVector aNewPrev(rPrev - aPoint); + const basegfx::B2DVector aNewNext(rNext - aPoint); + + if(getPrevControlVector(nIndex) != aNewPrev || getNextControlVector(nIndex) != aNewNext) + { + mpPolygon->setControlVectors(nIndex, aNewPrev, aNewNext); + } + } + + void B2DPolygon::resetPrevControlPoint(sal_uInt32 nIndex) + { + if(areControlPointsUsed() && !getPrevControlVector(nIndex).equalZero()) + { + mpPolygon->setPrevControlVector(nIndex, B2DVector::getEmptyVector()); + } + } + + void B2DPolygon::resetNextControlPoint(sal_uInt32 nIndex) + { + if(areControlPointsUsed() && !getNextControlVector(nIndex).equalZero()) + { + mpPolygon->setNextControlVector(nIndex, B2DVector::getEmptyVector()); + } + } + + void B2DPolygon::resetControlPoints() + { + if(areControlPointsUsed()) + { + mpPolygon->resetControlVectors(); + } + } + + void B2DPolygon::appendBezierSegment( + const B2DPoint& rNextControlPoint, + const B2DPoint& rPrevControlPoint, + const B2DPoint& rPoint) + { + const B2DVector aNewNextVector(count() ? B2DVector(rNextControlPoint - getB2DPoint(count() - 1)) : B2DVector::getEmptyVector()); + const B2DVector aNewPrevVector(rPrevControlPoint - rPoint); + + if(aNewNextVector.equalZero() && aNewPrevVector.equalZero()) + { + mpPolygon->append(rPoint); + } + else + { + mpPolygon->appendBezierSegment(aNewNextVector, aNewPrevVector, rPoint); + } + } + + void B2DPolygon::appendQuadraticBezierSegment(const B2DPoint& rControlPoint, const B2DPoint& rPoint) + { + if (count() == 0) + { + mpPolygon->append(rPoint); + const double nX((rControlPoint.getX() * 2.0 + rPoint.getX()) / 3.0); + const double nY((rControlPoint.getY() * 2.0 + rPoint.getY()) / 3.0); + setPrevControlPoint(0, B2DPoint(nX, nY)); + } + else + { + const B2DPoint aPreviousPoint(getB2DPoint(count() - 1)); + + const double nX1((rControlPoint.getX() * 2.0 + aPreviousPoint.getX()) / 3.0); + const double nY1((rControlPoint.getY() * 2.0 + aPreviousPoint.getY()) / 3.0); + const double nX2((rControlPoint.getX() * 2.0 + rPoint.getX()) / 3.0); + const double nY2((rControlPoint.getY() * 2.0 + rPoint.getY()) / 3.0); + + appendBezierSegment(B2DPoint(nX1, nY1), B2DPoint(nX2, nY2), rPoint); + } + } + + bool B2DPolygon::areControlPointsUsed() const + { + return mpPolygon->areControlPointsUsed(); + } + + bool B2DPolygon::isPrevControlPointUsed(sal_uInt32 nIndex) const + { + return (areControlPointsUsed() && !getPrevControlVector(nIndex).equalZero()); + } + + bool B2DPolygon::isNextControlPointUsed(sal_uInt32 nIndex) const + { + return (areControlPointsUsed() && !getNextControlVector(nIndex).equalZero()); + } + + B2VectorContinuity B2DPolygon::getContinuityInPoint(sal_uInt32 nIndex) const + { + if(areControlPointsUsed()) + { + const B2DVector& rPrev(getPrevControlVector(nIndex)); + const B2DVector& rNext(getNextControlVector(nIndex)); + + return getContinuity(rPrev, rNext); + } + else + { + return B2VectorContinuity::NONE; + } + } + + void B2DPolygon::getBezierSegment(sal_uInt32 nIndex, B2DCubicBezier& rTarget) const + { + const bool bNextIndexValidWithoutClose(nIndex + 1 < count()); + + if(bNextIndexValidWithoutClose || isClosed()) + { + const sal_uInt32 nNextIndex(bNextIndexValidWithoutClose ? nIndex + 1 : 0); + rTarget.setStartPoint(getB2DPoint(nIndex)); + rTarget.setEndPoint(getB2DPoint(nNextIndex)); + + if(areControlPointsUsed()) + { + rTarget.setControlPointA(rTarget.getStartPoint() + getNextControlVector(nIndex)); + rTarget.setControlPointB(rTarget.getEndPoint() + getPrevControlVector(nNextIndex)); + } + else + { + // no bezier, reset control points at rTarget + rTarget.setControlPointA(rTarget.getStartPoint()); + rTarget.setControlPointB(rTarget.getEndPoint()); + } + } + else + { + // no valid edge at all, reset rTarget to current point + const B2DPoint aPoint(getB2DPoint(nIndex)); + rTarget.setStartPoint(aPoint); + rTarget.setEndPoint(aPoint); + rTarget.setControlPointA(aPoint); + rTarget.setControlPointB(aPoint); + } + } + + B2DPolygon const & B2DPolygon::getDefaultAdaptiveSubdivision() const + { + return mpPolygon->getDefaultAdaptiveSubdivision(*this); + } + + B2DRange const & B2DPolygon::getB2DRange() const + { + return mpPolygon->getB2DRange(*this); + } + + void B2DPolygon::append(const B2DPolygon& rPoly, sal_uInt32 nIndex, sal_uInt32 nCount) + { + assert(nIndex + nCount <= rPoly.count()); + + if(!nCount) + { + nCount = rPoly.count() - nIndex; + if (!nCount) + return; + } + + if(nIndex == 0 && nCount == rPoly.count()) + { + mpPolygon->append(*rPoly.mpPolygon); + } + else + { + mpPolygon->append(ImplB2DPolygon(*rPoly.mpPolygon, nIndex, nCount)); + } + } + + void B2DPolygon::remove(sal_uInt32 nIndex, sal_uInt32 nCount) + { + if(nCount) + { + mpPolygon->remove(nIndex, nCount); + } + } + + void B2DPolygon::clear() + { + *mpPolygon = ImplB2DPolygon(); + } + + bool B2DPolygon::isClosed() const + { + return mpPolygon->isClosed(); + } + + void B2DPolygon::setClosed(bool bNew) + { + if(isClosed() != bNew) + { + mpPolygon->setClosed(bNew); + } + } + + void B2DPolygon::flip() + { + if(count() > 1) + { + mpPolygon->flip(); + } + } + + bool B2DPolygon::hasDoublePoints() const + { + return (count() > 1 && mpPolygon->hasDoublePoints()); + } + + void B2DPolygon::removeDoublePoints() + { + if(hasDoublePoints()) + { + mpPolygon->removeDoublePointsAtBeginEnd(); + mpPolygon->removeDoublePointsWholeTrack(); + } + } + + void B2DPolygon::transform(const B2DHomMatrix& rMatrix) + { + if(count() && !rMatrix.isIdentity()) + { + mpPolygon->transform(rMatrix); + } + } + + void B2DPolygon::addOrReplaceSystemDependentDataInternal(SystemDependentData_SharedPtr& rData) const + { + mpPolygon->addOrReplaceSystemDependentData(rData); + } + + SystemDependentData_SharedPtr B2DPolygon::getSystemDependantDataInternal(size_t hash_code) const + { + return mpPolygon->getSystemDependentData(hash_code); + } + +} // end of namespace basegfx + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/basegfx/source/polygon/b2dpolygonclipper.cxx b/basegfx/source/polygon/b2dpolygonclipper.cxx new file mode 100644 index 0000000000..e2f1f06038 --- /dev/null +++ b/basegfx/source/polygon/b2dpolygonclipper.cxx @@ -0,0 +1,824 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * 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 . + */ + +#include <basegfx/polygon/b2dpolygonclipper.hxx> +#include <basegfx/polygon/b2dpolygontools.hxx> +#include <basegfx/numeric/ftools.hxx> +#include <basegfx/polygon/b2dpolypolygoncutter.hxx> +#include <basegfx/polygon/b2dpolygoncutandtouch.hxx> +#include <basegfx/polygon/b2dpolypolygontools.hxx> +#include <basegfx/curve/b2dcubicbezier.hxx> +#include <basegfx/utils/rectcliptools.hxx> +#include <sal/log.hxx> + +namespace basegfx::utils +{ + B2DPolyPolygon clipPolygonOnParallelAxis(const B2DPolygon& rCandidate, bool bParallelToXAxis, bool bAboveAxis, double fValueOnOtherAxis, bool bStroke) + { + B2DPolyPolygon aRetval; + + if(rCandidate.count()) + { + const B2DRange aCandidateRange(getRange(rCandidate)); + + if(bParallelToXAxis && fTools::moreOrEqual(aCandidateRange.getMinY(), fValueOnOtherAxis)) + { + // completely above and on the clip line. also true for curves. + if(bAboveAxis) + { + // add completely + aRetval.append(rCandidate); + } + } + else if(bParallelToXAxis && fTools::lessOrEqual(aCandidateRange.getMaxY(), fValueOnOtherAxis)) + { + // completely below and on the clip line. also true for curves. + if(!bAboveAxis) + { + // add completely + aRetval.append(rCandidate); + } + } + else if(!bParallelToXAxis && fTools::moreOrEqual(aCandidateRange.getMinX(), fValueOnOtherAxis)) + { + // completely right of and on the clip line. also true for curves. + if(bAboveAxis) + { + // add completely + aRetval.append(rCandidate); + } + } + else if(!bParallelToXAxis && fTools::lessOrEqual(aCandidateRange.getMaxX(), fValueOnOtherAxis)) + { + // completely left of and on the clip line. also true for curves. + if(!bAboveAxis) + { + // add completely + aRetval.append(rCandidate); + } + } + else + { + // add cuts with axis to polygon, including bezier segments + // Build edge to cut with. Make it a little big longer than needed for + // numerical stability. We want to cut against the edge seen as endless + // ray here, but addPointsAtCuts() will limit itself to the + // edge's range ]0.0 .. 1.0[. + const double fSmallExtension((aCandidateRange.getWidth() + aCandidateRange.getHeight()) * (0.5 * 0.1)); + const B2DPoint aStart( + bParallelToXAxis ? aCandidateRange.getMinX() - fSmallExtension : fValueOnOtherAxis, + bParallelToXAxis ? fValueOnOtherAxis : aCandidateRange.getMinY() - fSmallExtension); + const B2DPoint aEnd( + bParallelToXAxis ? aCandidateRange.getMaxX() + fSmallExtension : fValueOnOtherAxis, + bParallelToXAxis ? fValueOnOtherAxis : aCandidateRange.getMaxY() + fSmallExtension); + const B2DPolygon aCandidate(addPointsAtCuts(rCandidate, aStart, aEnd)); + const sal_uInt32 nPointCount(aCandidate.count()); + const sal_uInt32 nEdgeCount(aCandidate.isClosed() ? nPointCount : nPointCount - 1); + B2DCubicBezier aEdge; + B2DPolygon aRun; + + for(sal_uInt32 a(0); a < nEdgeCount; a++) + { + aCandidate.getBezierSegment(a, aEdge); + const B2DPoint aTestPoint(aEdge.interpolatePoint(0.5)); + const bool bInside(bParallelToXAxis ? + fTools::moreOrEqual(aTestPoint.getY(), fValueOnOtherAxis) == bAboveAxis : + fTools::moreOrEqual(aTestPoint.getX(), fValueOnOtherAxis) == bAboveAxis); + + if(bInside) + { + if(!aRun.count() || !aRun.getB2DPoint(aRun.count() - 1).equal(aEdge.getStartPoint())) + { + aRun.append(aEdge.getStartPoint()); + } + + if(aEdge.isBezier()) + { + aRun.appendBezierSegment(aEdge.getControlPointA(), aEdge.getControlPointB(), aEdge.getEndPoint()); + } + else + { + aRun.append(aEdge.getEndPoint()); + } + } + else + { + if(bStroke && aRun.count()) + { + aRetval.append(aRun); + aRun.clear(); + } + } + } + + if(aRun.count()) + { + if(bStroke) + { + // try to merge this last and first polygon; they may have been + // the former polygon's start/end point + if(aRetval.count()) + { + const B2DPolygon aStartPolygon(aRetval.getB2DPolygon(0)); + + if(aStartPolygon.count() && aStartPolygon.getB2DPoint(0).equal(aRun.getB2DPoint(aRun.count() - 1))) + { + // append start polygon to aRun, remove from result set + aRun.append(aStartPolygon); aRun.removeDoublePoints(); + aRetval.remove(0); + } + } + + aRetval.append(aRun); + } + else + { + // set closed flag and correct last point (which is added double now). + closeWithGeometryChange(aRun); + aRetval.append(aRun); + } + } + } + } + + return aRetval; + } + + B2DPolyPolygon clipPolyPolygonOnParallelAxis(const B2DPolyPolygon& rCandidate, bool bParallelToXAxis, bool bAboveAxis, double fValueOnOtherAxis, bool bStroke) + { + B2DPolyPolygon aRetval; + + for(const auto& rB2DPolygon : rCandidate ) + { + const B2DPolyPolygon aClippedPolyPolygon(clipPolygonOnParallelAxis(rB2DPolygon, bParallelToXAxis, bAboveAxis, fValueOnOtherAxis, bStroke)); + + if(aClippedPolyPolygon.count()) + { + aRetval.append(aClippedPolyPolygon); + } + } + + return aRetval; + } + + B2DPolyPolygon clipPolygonOnRange(const B2DPolygon& rCandidate, const B2DRange& rRange, bool bInside, bool bStroke) + { + const sal_uInt32 nCount(rCandidate.count()); + B2DPolyPolygon aRetval; + + if(!nCount) + { + // source is empty + return aRetval; + } + + if(rRange.isEmpty()) + { + if(bInside) + { + // nothing is inside an empty range + return aRetval; + } + else + { + // everything is outside an empty range + return B2DPolyPolygon(rCandidate); + } + } + + const B2DRange aCandidateRange(getRange(rCandidate)); + + if(rRange.isInside(aCandidateRange)) + { + // candidate is completely inside given range + if(bInside) + { + // nothing to do + return B2DPolyPolygon(rCandidate); + } + else + { + // nothing is outside, then + return aRetval; + } + } + + if(!bInside) + { + // cutting off the outer parts of filled polygons at parallel + // lines to the axes is only possible for the inner part, not for + // the outer part which means cutting a hole into the original polygon. + // This is because the inner part is a logical AND-operation of + // the four implied half-planes, but the outer part is not. + // It is possible for strokes, but with creating unnecessary extra + // cuts, so using clipPolygonOnPolyPolygon is better there, too. + // This needs to be done with the topology knowledge and is unfortunately + // more expensive, too. + const B2DPolygon aClip(createPolygonFromRect(rRange)); + + return clipPolygonOnPolyPolygon(rCandidate, B2DPolyPolygon(aClip), bInside, bStroke); + } + + // clip against the four axes of the range + // against X-Axis, lower value + aRetval = clipPolygonOnParallelAxis(rCandidate, true, bInside, rRange.getMinY(), bStroke); + + if(aRetval.count()) + { + // against Y-Axis, lower value + if(aRetval.count() == 1) + { + aRetval = clipPolygonOnParallelAxis(aRetval.getB2DPolygon(0), false, bInside, rRange.getMinX(), bStroke); + } + else + { + aRetval = clipPolyPolygonOnParallelAxis(aRetval, false, bInside, rRange.getMinX(), bStroke); + } + + if(aRetval.count()) + { + // against X-Axis, higher value + if(aRetval.count() == 1) + { + aRetval = clipPolygonOnParallelAxis(aRetval.getB2DPolygon(0), true, false, rRange.getMaxY(), bStroke); + } + else + { + aRetval = clipPolyPolygonOnParallelAxis(aRetval, true, false, rRange.getMaxY(), bStroke); + } + + if(aRetval.count()) + { + // against Y-Axis, higher value + if(aRetval.count() == 1) + { + aRetval = clipPolygonOnParallelAxis(aRetval.getB2DPolygon(0), false, false, rRange.getMaxX(), bStroke); + } + else + { + aRetval = clipPolyPolygonOnParallelAxis(aRetval, false, false, rRange.getMaxX(), bStroke); + } + } + } + } + + return aRetval; + } + + B2DPolyPolygon clipPolyPolygonOnRange(const B2DPolyPolygon& rCandidate, const B2DRange& rRange, bool bInside, bool bStroke) + { + B2DPolyPolygon aRetval; + + if(!rCandidate.count()) + { + // source is empty + return aRetval; + } + + if(rRange.isEmpty()) + { + if(bInside) + { + // nothing is inside an empty range + return aRetval; + } + else + { + // everything is outside an empty range + return rCandidate; + } + } + + if(bInside) + { + for( const auto& rClippedPoly : rCandidate) + { + const B2DPolyPolygon aClippedPolyPolygon(clipPolygonOnRange(rClippedPoly , rRange, bInside, bStroke)); + + if(aClippedPolyPolygon.count()) + { + aRetval.append(aClippedPolyPolygon); + } + } + } + else + { + // for details, see comment in clipPolygonOnRange for the "cutting off + // the outer parts of filled polygons at parallel lines" explanations + const B2DPolygon aClip(createPolygonFromRect(rRange)); + + return clipPolyPolygonOnPolyPolygon(rCandidate, B2DPolyPolygon(aClip), bInside, bStroke); + } + + return aRetval; + } + + B2DPolyPolygon clipPolyPolygonOnPolyPolygon(const B2DPolyPolygon& rCandidate, const B2DPolyPolygon& rClip, + bool bInside, bool bStroke, size_t* pPointLimit) + { + B2DPolyPolygon aRetval; + + if(rCandidate.count() && rClip.count()) + { + // one or both are no rectangle - go the hard way and clip PolyPolygon + // against PolyPolygon... + if(bStroke) + { + // line clipping, create line snippets by first adding all cut points and + // then marching along the edges and detecting if they are inside or outside + // the clip polygon + for(const auto& rPolygon : rCandidate) + { + // add cuts with clip to polygon, including bezier segments + const B2DPolygon aCandidate(addPointsAtCuts(rPolygon, rClip)); + const sal_uInt32 nPointCount(aCandidate.count()); + const sal_uInt32 nEdgeCount(aCandidate.isClosed() ? nPointCount : nPointCount - 1); + B2DCubicBezier aEdge; + B2DPolygon aRun; + + for(sal_uInt32 b(0); b < nEdgeCount; b++) + { + aCandidate.getBezierSegment(b, aEdge); + const B2DPoint aTestPoint(aEdge.interpolatePoint(0.5)); + const bool bIsInside(utils::isInside(rClip, aTestPoint) == bInside); + + if(bIsInside) + { + if(!aRun.count()) + { + aRun.append(aEdge.getStartPoint()); + } + + if(aEdge.isBezier()) + { + aRun.appendBezierSegment(aEdge.getControlPointA(), aEdge.getControlPointB(), aEdge.getEndPoint()); + } + else + { + aRun.append(aEdge.getEndPoint()); + } + } + else + { + if(aRun.count()) + { + aRetval.append(aRun); + aRun.clear(); + } + } + } + + if(aRun.count()) + { + // try to merge this last and first polygon; they may have been + // the former polygon's start/end point + if(aRetval.count()) + { + const B2DPolygon aStartPolygon(aRetval.getB2DPolygon(0)); + + if(aStartPolygon.count() && aStartPolygon.getB2DPoint(0).equal(aRun.getB2DPoint(aRun.count() - 1))) + { + // append start polygon to aRun, remove from result set + aRun.append(aStartPolygon); aRun.removeDoublePoints(); + aRetval.remove(0); + } + } + + aRetval.append(aRun); + } + } + } + else + { + // check for simplification with ranges if !bStroke (handling as stroke is more simple), + // but also only when bInside, else the simplification may lead to recursive calls (see + // calls to clipPolyPolygonOnPolyPolygon in clipPolyPolygonOnRange and clipPolygonOnRange) + if (bInside && basegfx::utils::isRectangle(rClip)) + { + // #i125349# detect if both given PolyPolygons are indeed ranges + if (basegfx::utils::isRectangle(rCandidate)) + { + // both are rectangle + if(rCandidate.getB2DRange().equal(rClip.getB2DRange())) + { + // if both are equal -> no change + return rCandidate; + } + else + { + // not equal -> create new intersection from both ranges, + // but much cheaper based on the ranges + basegfx::B2DRange aIntersectionRange(rCandidate.getB2DRange()); + + aIntersectionRange.intersect(rClip.getB2DRange()); + + if(aIntersectionRange.isEmpty()) + { + // no common IntersectionRange -> the clip will be empty + return B2DPolyPolygon(); + } + else + { + // use common aIntersectionRange as result, convert + // to expected utils::PolyPolygon form + return basegfx::B2DPolyPolygon( + basegfx::utils::createPolygonFromRect(aIntersectionRange)); + } + } + } + else + { + // rClip is rectangle -> clip rCandidate on rRectangle, use the much + // cheaper and numerically more stable clipping against a range + return clipPolyPolygonOnRange(rCandidate, rClip.getB2DRange(), bInside, bStroke); + } + } + + // area clipping + + // First solve all polygon-self and polygon-polygon intersections. + // Also get rid of some not-needed polygons (neutral, no area -> when + // no intersections, these are tubes). + // Now it is possible to correct the orientations in the cut-free + // polygons to values corresponding to painting the utils::PolyPolygon with + // a XOR-WindingRule. + B2DPolyPolygon aMergePolyPolygonA = solveCrossovers(rClip); + aMergePolyPolygonA = stripNeutralPolygons(aMergePolyPolygonA); + aMergePolyPolygonA = correctOrientations(aMergePolyPolygonA); + + if(!bInside) + { + // if we want to get the outside of the clip polygon, make + // it a 'Hole' in topological sense + aMergePolyPolygonA.flip(); + } + + + // prepare 2nd source polygon in same way + B2DPolyPolygon aMergePolyPolygonB = solveCrossovers(rCandidate, pPointLimit); + + if (pPointLimit && !*pPointLimit) + { + SAL_WARN("basegfx", "clipPolyPolygonOnPolyPolygon hit point limit"); + return aRetval; + } + + aMergePolyPolygonB = stripNeutralPolygons(aMergePolyPolygonB); + aMergePolyPolygonB = correctOrientations(aMergePolyPolygonB); + + // to clip against each other, concatenate and solve all + // polygon-polygon crossovers. polygon-self do not need to + // be solved again, they were solved in the preparation. + aRetval.append(aMergePolyPolygonA); + aRetval.append(aMergePolyPolygonB); + aRetval = solveCrossovers(aRetval, pPointLimit); + + // now remove neutral polygons (closed, but no area). In a last + // step throw away all polygons which have a depth of less than 1 + // which means there was no logical AND at their position. For the + // not-inside solution, the clip was flipped to define it as 'Hole', + // so the removal rule is different here; remove all with a depth + // of less than 0 (aka holes). + aRetval = stripNeutralPolygons(aRetval); + aRetval = stripDispensablePolygons(aRetval, bInside); + } + } + + return aRetval; + } + + B2DPolyPolygon clipPolygonOnPolyPolygon(const B2DPolygon& rCandidate, const B2DPolyPolygon& rClip, bool bInside, bool bStroke) + { + B2DPolyPolygon aRetval; + + if(rCandidate.count() && rClip.count()) + { + aRetval = clipPolyPolygonOnPolyPolygon(B2DPolyPolygon(rCandidate), rClip, bInside, bStroke); + } + + return aRetval; + } + + namespace { + + /* + * let a plane be defined as + * + * v.n+d=0 + * + * and a ray be defined as + * + * a+(b-a)*t=0 + * + * substitute and rearranging yields + * + * t = -(a.n+d)/(n.(b-a)) + * + * if the denominator is zero, the line is either + * contained in the plane or parallel to the plane. + * in either case, there is no intersection. + * if numerator and denominator are both zero, the + * ray is contained in the plane. + * + */ + struct scissor_plane { + double nx,ny; // plane normal + double d; // [-] minimum distance from origin + sal_uInt32 clipmask; // clipping mask, e.g. 1000 1000 + }; + + } + + /* + * + * polygon clipping rules (straight out of Foley and Van Dam) + * =========================================================== + * current |next |emit + * ____________________________________ + * inside |inside |next + * inside |outside |intersect with clip plane + * outside |outside |nothing + * outside |inside |intersect with clip plane followed by next + * + */ + static sal_uInt32 scissorLineSegment( ::basegfx::B2DPoint *in_vertex, // input buffer + sal_uInt32 in_count, // number of verts in input buffer + ::basegfx::B2DPoint *out_vertex, // output buffer + scissor_plane const *pPlane, // scissoring plane + const ::basegfx::B2DRectangle &rR ) // clipping rectangle + { + + sal_uInt32 out_count=0; + + // process all the verts + for(sal_uInt32 i=0; i<in_count; i++) { + + // vertices are relative to the coordinate + // system defined by the rectangle. + ::basegfx::B2DPoint *curr = &in_vertex[i]; + ::basegfx::B2DPoint *next = &in_vertex[(i+1)%in_count]; + + // perform clipping judgement & mask against current plane. + sal_uInt32 clip = pPlane->clipmask & ((getCohenSutherlandClipFlags(*curr,rR)<<4)|getCohenSutherlandClipFlags(*next,rR)); + + if(clip==0) { // both verts are inside + out_vertex[out_count++] = *next; + } + else if((clip&0x0f) && (clip&0xf0)) { // both verts are outside + } + else if((clip&0x0f) && (clip&0xf0)==0) { // curr is inside, next is outside + + // direction vector from 'current' to 'next', *not* normalized + // to bring 't' into the [0<=x<=1] interval. + ::basegfx::B2DPoint dir((*next)-(*curr)); + + double denominator = pPlane->nx*dir.getX() + + pPlane->ny*dir.getY(); + double numerator = pPlane->nx*curr->getX() + + pPlane->ny*curr->getY() + + pPlane->d; + double t = -numerator/denominator; + + // calculate the actual point of intersection + ::basegfx::B2DPoint intersection( curr->getX()+t*dir.getX(), + curr->getY()+t*dir.getY() ); + + out_vertex[out_count++] = intersection; + } + else if((clip&0x0f)==0 && (clip&0xf0)) { // curr is outside, next is inside + + // direction vector from 'current' to 'next', *not* normalized + // to bring 't' into the [0<=x<=1] interval. + ::basegfx::B2DPoint dir((*next)-(*curr)); + + double denominator = pPlane->nx*dir.getX() + + pPlane->ny*dir.getY(); + double numerator = pPlane->nx*curr->getX() + + pPlane->ny*curr->getY() + + pPlane->d; + double t = -numerator/denominator; + + // calculate the actual point of intersection + ::basegfx::B2DPoint intersection( curr->getX()+t*dir.getX(), + curr->getY()+t*dir.getY() ); + + out_vertex[out_count++] = intersection; + out_vertex[out_count++] = *next; + } + } + + return out_count; + } + + B2DPolygon clipTriangleListOnRange( const B2DPolygon& rCandidate, + const B2DRange& rRange ) + { + B2DPolygon aResult; + + if( !(rCandidate.count()%3) ) + { + const int scissor_plane_count = 4; + + scissor_plane sp[scissor_plane_count]; + + sp[0].nx = +1.0; + sp[0].ny = +0.0; + sp[0].d = -(rRange.getMinX()); + sp[0].clipmask = (RectClipFlags::LEFT << 4) | RectClipFlags::LEFT; // 0001 0001 + sp[1].nx = -1.0; + sp[1].ny = +0.0; + sp[1].d = +(rRange.getMaxX()); + sp[1].clipmask = (RectClipFlags::RIGHT << 4) | RectClipFlags::RIGHT; // 0010 0010 + sp[2].nx = +0.0; + sp[2].ny = +1.0; + sp[2].d = -(rRange.getMinY()); + sp[2].clipmask = (RectClipFlags::TOP << 4) | RectClipFlags::TOP; // 0100 0100 + sp[3].nx = +0.0; + sp[3].ny = -1.0; + sp[3].d = +(rRange.getMaxY()); + sp[3].clipmask = (RectClipFlags::BOTTOM << 4) | RectClipFlags::BOTTOM; // 1000 1000 + + // retrieve the number of vertices of the triangulated polygon + const sal_uInt32 nVertexCount = rCandidate.count(); + + if(nVertexCount) + { + // Upper bound for the maximal number of vertices when intersecting an + // axis-aligned rectangle with a triangle in E2 + + // The rectangle and the triangle are in general position, and have 4 and 3 + // vertices, respectively. + + // Lemma: Since the rectangle is a convex polygon ( see + // http://mathworld.wolfram.com/ConvexPolygon.html for a definition), and + // has no holes, it follows that any straight line will intersect the + // rectangle's border line at utmost two times (with the usual + // tie-breaking rule, if the intersection exactly hits an already existing + // rectangle vertex, that this intersection is only attributed to one of + // the adjoining edges). Thus, having a rectangle intersected with + // a half-plane (one side of a straight line denotes 'inside', the + // other 'outside') will at utmost add _one_ vertex to the resulting + // intersection polygon (adding two intersection vertices, and removing at + // least one rectangle vertex): + + // * + // +--+-----------------+ + // | * | + // |* | + // + | + // *| | + // * | | + // +--------------------+ + + // Proof: If the straight line intersects the rectangle two + // times, it does so for distinct edges, i.e. the intersection has + // minimally one of the rectangle's vertices on either side of the straight + // line (but maybe more). Thus, the intersection with a half-plane has + // minimally _one_ rectangle vertex removed from the resulting clip + // polygon, and therefore, a clip against a half-plane has the net effect + // of adding at utmost _one_ vertex to the resulting clip polygon. + + // Theorem: The intersection of a rectangle and a triangle results in a + // polygon with at utmost 7 vertices. + + // Proof: The inside of the triangle can be described as the consecutive + // intersection with three half-planes. Together with the lemma above, this + // results in at utmost 3 additional vertices added to the already existing 4 + // rectangle vertices. + + // This upper bound is attained with the following example configuration: + + // * + // *** + // ** * + // ** * + // ** * + // ** * + // ** * + // ** * + // ** * + // ** * + // ** * + // ----*2--------3 * + // | ** |* + // 1* 4 + // **| *| + // ** | * | + // **| * | + // 7* * | + // --*6-----5----- + // ** * + // ** + + // As we need to scissor all triangles against the + // output rectangle we employ an output buffer for the + // resulting vertices. the question is how large this + // buffer needs to be compared to the number of + // incoming vertices. this buffer needs to hold at + // most the number of original vertices times '7'. see + // figure above for an example. scissoring triangles + // with the cohen-sutherland line clipping algorithm + // as implemented here will result in a triangle fan + // which will be rendered as separate triangles to + // avoid pipeline stalls for each scissored + // triangle. creating separate triangles from a + // triangle fan produces (n-2)*3 vertices where n is + // the number of vertices of the original triangle + // fan. for the maximum number of 7 vertices of + // resulting triangle fans we therefore need 15 times + // the number of original vertices. + + //const size_t nBufferSize = sizeof(vertex)*(nVertexCount*16); + //vertex *pVertices = (vertex*)alloca(nBufferSize); + //sal_uInt32 nNumOutput = 0; + + // we need to clip this triangle against the output rectangle + // to ensure that the resulting texture coordinates are in + // the valid range from [0<=st<=1]. under normal circumstances + // we could use the BORDERCOLOR renderstate but some cards + // seem to ignore this feature. + ::basegfx::B2DPoint stack[3]; + unsigned int clipflag = 0; + + for(sal_uInt32 nIndex=0; nIndex<nVertexCount; ++nIndex) + { + // rotate stack + stack[0] = stack[1]; + stack[1] = stack[2]; + stack[2] = rCandidate.getB2DPoint(nIndex); + + // clipping judgement + clipflag |= unsigned(!(rRange.isInside(stack[2]))); + + if(nIndex > 1) + { + // consume vertices until a single separate triangle has been visited. + if(!((nIndex+1)%3)) + { + // if any of the last three vertices was outside + // we need to scissor against the destination rectangle + if(clipflag & 7) + { + ::basegfx::B2DPoint buf0[16]; + ::basegfx::B2DPoint buf1[16]; + + sal_uInt32 vertex_count = 3; + + // clip against all 4 planes passing the result of + // each plane as the input to the next using a double buffer + vertex_count = scissorLineSegment(stack,vertex_count,buf1,&sp[0],rRange); + vertex_count = scissorLineSegment(buf1,vertex_count,buf0,&sp[1],rRange); + vertex_count = scissorLineSegment(buf0,vertex_count,buf1,&sp[2],rRange); + vertex_count = scissorLineSegment(buf1,vertex_count,buf0,&sp[3],rRange); + + if(vertex_count >= 3) + { + // convert triangle fan back to triangle list. + ::basegfx::B2DPoint v0(buf0[0]); + ::basegfx::B2DPoint v1(buf0[1]); + for(sal_uInt32 i=2; i<vertex_count; ++i) + { + ::basegfx::B2DPoint v2(buf0[i]); + aResult.append(v0); + aResult.append(v1); + aResult.append(v2); + v1 = v2; + } + } + } + else + { + // the last triangle has not been altered, simply copy to result + for(const basegfx::B2DPoint & i : stack) + aResult.append(i); + } + } + } + + clipflag <<= 1; + } + } + } + + return aResult; + } + +} // end of namespace + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/basegfx/source/polygon/b2dpolygoncutandtouch.cxx b/basegfx/source/polygon/b2dpolygoncutandtouch.cxx new file mode 100644 index 0000000000..c91f0ec48f --- /dev/null +++ b/basegfx/source/polygon/b2dpolygoncutandtouch.cxx @@ -0,0 +1,1077 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * 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 . + */ + +#include <basegfx/polygon/b2dpolygoncutandtouch.hxx> +#include <osl/diagnose.h> +#include <sal/log.hxx> +#include <basegfx/numeric/ftools.hxx> +#include <basegfx/point/b2dpoint.hxx> +#include <basegfx/vector/b2dvector.hxx> +#include <basegfx/range/b2drange.hxx> +#include <basegfx/polygon/b2dpolygontools.hxx> +#include <basegfx/curve/b2dcubicbezier.hxx> + +#include <vector> +#include <algorithm> +#include <memory> + +#define SUBDIVIDE_FOR_CUT_TEST_COUNT (50) + +namespace basegfx +{ + namespace + { + + class temporaryPoint + { + B2DPoint maPoint; // the new point + sal_uInt32 mnIndex; // index after which to insert + double mfCut; // parametric cut description [0.0 .. 1.0] + + public: + temporaryPoint(const B2DPoint& rNewPoint, sal_uInt32 nIndex, double fCut) + : maPoint(rNewPoint), + mnIndex(nIndex), + mfCut(fCut) + { + } + + bool operator<(const temporaryPoint& rComp) const + { + if(mnIndex == rComp.mnIndex) + { + return (mfCut < rComp.mfCut); + } + + return (mnIndex < rComp.mnIndex); + } + + const B2DPoint& getPoint() const { return maPoint; } + sal_uInt32 getIndex() const { return mnIndex; } + double getCut() const { return mfCut; } + }; + + typedef std::vector< temporaryPoint > temporaryPointVector; + + class temporaryPolygonData + { + B2DPolygon maPolygon; + B2DRange maRange; + temporaryPointVector maPoints; + + public: + const B2DPolygon& getPolygon() const { return maPolygon; } + void setPolygon(const B2DPolygon& rNew) { maPolygon = rNew; maRange = utils::getRange(maPolygon); } + const B2DRange& getRange() const { return maRange; } + temporaryPointVector& getTemporaryPointVector() { return maPoints; } + }; + + B2DPolygon mergeTemporaryPointsAndPolygon(const B2DPolygon& rCandidate, temporaryPointVector& rTempPoints) + { + // #i76891# mergeTemporaryPointsAndPolygon redesigned to be able to correctly handle + // single edges with/without control points + // #i101491# added counter for non-changing element count + const sal_uInt32 nTempPointCount(rTempPoints.size()); + + if(nTempPointCount) + { + B2DPolygon aRetval; + const sal_uInt32 nCount(rCandidate.count()); + + if(nCount) + { + // sort temp points to assure increasing fCut values and increasing indices + std::sort(rTempPoints.begin(), rTempPoints.end()); + + // prepare loop + B2DCubicBezier aEdge; + sal_uInt32 nNewInd(0); + + // add start point + aRetval.append(rCandidate.getB2DPoint(0)); + + for(sal_uInt32 a(0); a < nCount; a++) + { + // get edge + rCandidate.getBezierSegment(a, aEdge); + + if(aEdge.isBezier()) + { + // control vectors involved for this edge + double fLeftStart(0.0); + + // now add all points targeted to be at this index + while (nNewInd < nTempPointCount && rTempPoints[nNewInd].getIndex() == a && fLeftStart < 1.0) + { + const temporaryPoint& rTempPoint = rTempPoints[nNewInd++]; + + // split curve segment. Splits need to come sorted and need to be < 1.0. Also, + // since original segment is consumed from left to right, the cut values need + // to be scaled to the remaining part + B2DCubicBezier aLeftPart; + const double fRelativeSplitPoint((rTempPoint.getCut() - fLeftStart) / (1.0 - fLeftStart)); + aEdge.split(fRelativeSplitPoint, &aLeftPart, &aEdge); + fLeftStart = rTempPoint.getCut(); + + // add left bow + aRetval.appendBezierSegment(aLeftPart.getControlPointA(), aLeftPart.getControlPointB(), rTempPoint.getPoint()); + } + + // add remaining bow + aRetval.appendBezierSegment(aEdge.getControlPointA(), aEdge.getControlPointB(), aEdge.getEndPoint()); + } + else + { + // add all points targeted to be at this index + while(nNewInd < nTempPointCount && rTempPoints[nNewInd].getIndex() == a) + { + const temporaryPoint& rTempPoint = rTempPoints[nNewInd++]; + const B2DPoint& aNewPoint(rTempPoint.getPoint()); + + // do not add points double + if(!aRetval.getB2DPoint(aRetval.count() - 1).equal(aNewPoint)) + { + aRetval.append(aNewPoint); + } + } + + // add edge end point + aRetval.append(aEdge.getEndPoint()); + } + } + } + + if(rCandidate.isClosed()) + { + // set closed flag and correct last point (which is added double now). + utils::closeWithGeometryChange(aRetval); + } + + return aRetval; + } + else + { + return rCandidate; + } + } + + void adaptAndTransferCutsWithBezierSegment( + const temporaryPointVector& rPointVector, const B2DPolygon& rPolygon, + sal_uInt32 nInd, temporaryPointVector& rTempPoints) + { + // assuming that the subdivision to create rPolygon used equidistant pieces + // (as in adaptiveSubdivideByCount) it is now possible to calculate back the + // cut positions in the polygon to relative cut positions on the original bezier + // segment. + const sal_uInt32 nEdgeCount(rPolygon.count() ? rPolygon.count() - 1 : 0); + + if(!rPointVector.empty() && nEdgeCount) + { + for( const auto& rTempPoint : rPointVector ) + { + const double fCutPosInPolygon(static_cast<double>(rTempPoint.getIndex()) + rTempPoint.getCut()); + const double fRelativeCutPos(fCutPosInPolygon / static_cast<double>(nEdgeCount)); + rTempPoints.emplace_back(rTempPoint.getPoint(), nInd, fRelativeCutPos); + } + } + } + + } // end of anonymous namespace +} // end of namespace basegfx + +namespace basegfx +{ + namespace + { + + // predefines for calls to this methods before method implementation + + void findCuts(const B2DPolygon& rCandidate, temporaryPointVector& rTempPoints, size_t* pPointLimit = nullptr); + void findTouches(const B2DPolygon& rEdgePolygon, const B2DPolygon& rPointPolygon, temporaryPointVector& rTempPoints); + void findCuts(const B2DPolygon& rCandidateA, const B2DPolygon& rCandidateB, temporaryPointVector& rTempPointsA, temporaryPointVector& rTempPointsB); + + void findEdgeCutsTwoEdges( + const B2DPoint& rCurrA, const B2DPoint& rNextA, + const B2DPoint& rCurrB, const B2DPoint& rNextB, + sal_uInt32 nIndA, sal_uInt32 nIndB, + temporaryPointVector& rTempPointsA, temporaryPointVector& rTempPointsB) + { + // no null length edges + if(rCurrA.equal(rNextA) || rCurrB.equal(rNextB)) + return; + + // no common start/end points, this can be no cuts + if(rCurrB.equal(rCurrA) || rCurrB.equal(rNextA) || rNextB.equal(rCurrA) || rNextB.equal(rNextA)) + return; + + const B2DVector aVecA(rNextA - rCurrA); + const B2DVector aVecB(rNextB - rCurrB); + double fCut(aVecA.cross(aVecB)); + + if(fTools::equalZero(fCut)) + return; + + const double fZero(0.0); + const double fOne(1.0); + fCut = (aVecB.getY() * (rCurrB.getX() - rCurrA.getX()) + aVecB.getX() * (rCurrA.getY() - rCurrB.getY())) / fCut; + + if (!fTools::betweenOrEqualEither(fCut, fZero, fOne)) + return; + + // it's a candidate, but also need to test parameter value of cut on line 2 + double fCut2; + + // choose the more precise version + if(fabs(aVecB.getX()) > fabs(aVecB.getY())) + { + fCut2 = (rCurrA.getX() + (fCut * aVecA.getX()) - rCurrB.getX()) / aVecB.getX(); + } + else + { + fCut2 = (rCurrA.getY() + (fCut * aVecA.getY()) - rCurrB.getY()) / aVecB.getY(); + } + + if (fTools::betweenOrEqualEither(fCut2, fZero, fOne)) + { + // cut is in range, add point. Two edges can have only one cut, but + // add a cut point to each list. The lists may be the same for + // self intersections. + const B2DPoint aCutPoint(interpolate(rCurrA, rNextA, fCut)); + rTempPointsA.emplace_back(aCutPoint, nIndA, fCut); + rTempPointsB.emplace_back(aCutPoint, nIndB, fCut2); + } + } + + void findCutsAndTouchesAndCommonForBezier(const B2DPolygon& rCandidateA, const B2DPolygon& rCandidateB, temporaryPointVector& rTempPointsA, temporaryPointVector& rTempPointsB) + { + // #i76891# + // This new method is necessary since in findEdgeCutsBezierAndEdge and in findEdgeCutsTwoBeziers + // it is not sufficient to use findCuts() recursively. This will indeed find the cuts between the + // segments of the two temporarily adaptive subdivided bezier segments, but not the touches or + // equal points of them. + // It would be possible to find the touches using findTouches(), but at last with common points + // the adding of cut points (temporary points) would fail. But for these temporarily adaptive + // subdivided bezier segments, common points may be not very likely, but the bug shows that it + // happens. + // Touch points are a little bit more likely than common points. All in all it is best to use + // a specialized method here which can profit from knowing that it is working on a special + // family of B2DPolygons: no curve segments included and not closed. + OSL_ENSURE(!rCandidateA.areControlPointsUsed() && !rCandidateB.areControlPointsUsed(), "findCutsAndTouchesAndCommonForBezier only works with subdivided polygons (!)"); + OSL_ENSURE(!rCandidateA.isClosed() && !rCandidateB.isClosed(), "findCutsAndTouchesAndCommonForBezier only works with opened polygons (!)"); + const sal_uInt32 nPointCountA(rCandidateA.count()); + const sal_uInt32 nPointCountB(rCandidateB.count()); + + if(nPointCountA <= 1 || nPointCountB <= 1) + return; + + const sal_uInt32 nEdgeCountA(nPointCountA - 1); + const sal_uInt32 nEdgeCountB(nPointCountB - 1); + B2DPoint aCurrA(rCandidateA.getB2DPoint(0)); + + for(sal_uInt32 a(0); a < nEdgeCountA; a++) + { + const B2DPoint aNextA(rCandidateA.getB2DPoint(a + 1)); + const B2DRange aRangeA(aCurrA, aNextA); + B2DPoint aCurrB(rCandidateB.getB2DPoint(0)); + + for(sal_uInt32 b(0); b < nEdgeCountB; b++) + { + const B2DPoint aNextB(rCandidateB.getB2DPoint(b + 1)); + const B2DRange aRangeB(aCurrB, aNextB); + + if(aRangeA.overlaps(aRangeB)) + { + // no null length edges + if(!(aCurrA.equal(aNextA) || aCurrB.equal(aNextB))) + { + const B2DVector aVecA(aNextA - aCurrA); + const B2DVector aVecB(aNextB - aCurrB); + double fCutA(aVecA.cross(aVecB)); + + if(!fTools::equalZero(fCutA)) + { + const double fZero(0.0); + const double fOne(1.0); + fCutA = (aVecB.getY() * (aCurrB.getX() - aCurrA.getX()) + aVecB.getX() * (aCurrA.getY() - aCurrB.getY())) / fCutA; + + // use range [0.0 .. 1.0[, thus in the loop, all direct aCurrA cuts will be registered + // as 0.0 cut. The 1.0 cut will be registered in the next loop step + if(fTools::moreOrEqual(fCutA, fZero) && fTools::less(fCutA, fOne)) + { + // it's a candidate, but also need to test parameter value of cut on line 2 + double fCutB; + + // choose the more precise version + if(fabs(aVecB.getX()) > fabs(aVecB.getY())) + { + fCutB = (aCurrA.getX() + (fCutA * aVecA.getX()) - aCurrB.getX()) / aVecB.getX(); + } + else + { + fCutB = (aCurrA.getY() + (fCutA * aVecA.getY()) - aCurrB.getY()) / aVecB.getY(); + } + + // use range [0.0 .. 1.0[, thus in the loop, all direct aCurrA cuts will be registered + // as 0.0 cut. The 1.0 cut will be registered in the next loop step + if(fTools::moreOrEqual(fCutB, fZero) && fTools::less(fCutB, fOne)) + { + // cut is in both ranges. Add points for A and B + // #i111715# use fTools::equal instead of fTools::equalZero for better accuracy + if(fTools::equal(fCutA, fZero)) + { + // ignore for start point in first edge; this is handled + // by outer methods and would just produce a double point + if(a) + { + rTempPointsA.emplace_back(aCurrA, a, 0.0); + } + } + else + { + const B2DPoint aCutPoint(interpolate(aCurrA, aNextA, fCutA)); + rTempPointsA.emplace_back(aCutPoint, a, fCutA); + } + + // #i111715# use fTools::equal instead of fTools::equalZero for better accuracy + if(fTools::equal(fCutB, fZero)) + { + // ignore for start point in first edge; this is handled + // by outer methods and would just produce a double point + if(b) + { + rTempPointsB.emplace_back(aCurrB, b, 0.0); + } + } + else + { + const B2DPoint aCutPoint(interpolate(aCurrB, aNextB, fCutB)); + rTempPointsB.emplace_back(aCutPoint, b, fCutB); + } + } + } + } + } + } + + // prepare next step + aCurrB = aNextB; + } + + // prepare next step + aCurrA = aNextA; + } + } + + void findEdgeCutsBezierAndEdge( + const B2DCubicBezier& rCubicA, + const B2DPoint& rCurrB, const B2DPoint& rNextB, + sal_uInt32 nIndA, sal_uInt32 nIndB, + temporaryPointVector& rTempPointsA, temporaryPointVector& rTempPointsB) + { + // find all cuts between given bezier segment and edge. Add an entry to the tempPoints + // for each common point with the cut value describing the relative position on given + // bezier segment and edge. + B2DPolygon aTempPolygonA; + B2DPolygon aTempPolygonEdge; + temporaryPointVector aTempPointVectorA; + temporaryPointVector aTempPointVectorEdge; + + // create subdivided polygons and find cuts between them + // Keep adaptiveSubdivideByCount due to needed quality + aTempPolygonA.reserve(SUBDIVIDE_FOR_CUT_TEST_COUNT + 8); + aTempPolygonA.append(rCubicA.getStartPoint()); + rCubicA.adaptiveSubdivideByCount(aTempPolygonA, SUBDIVIDE_FOR_CUT_TEST_COUNT); + aTempPolygonEdge.append(rCurrB); + aTempPolygonEdge.append(rNextB); + + // #i76891# using findCuts recursively is not sufficient here + findCutsAndTouchesAndCommonForBezier(aTempPolygonA, aTempPolygonEdge, aTempPointVectorA, aTempPointVectorEdge); + + if(!aTempPointVectorA.empty()) + { + // adapt tempVector entries to segment + adaptAndTransferCutsWithBezierSegment(aTempPointVectorA, aTempPolygonA, nIndA, rTempPointsA); + } + + // append remapped tempVector entries for edge to tempPoints for edge + for(const temporaryPoint & rTempPoint : aTempPointVectorEdge) + { + rTempPointsB.emplace_back(rTempPoint.getPoint(), nIndB, rTempPoint.getCut()); + } + } + + void findEdgeCutsTwoBeziers( + const B2DCubicBezier& rCubicA, + const B2DCubicBezier& rCubicB, + sal_uInt32 nIndA, sal_uInt32 nIndB, + temporaryPointVector& rTempPointsA, temporaryPointVector& rTempPointsB) + { + // find all cuts between the two given bezier segments. Add an entry to the tempPoints + // for each common point with the cut value describing the relative position on given + // bezier segments. + B2DPolygon aTempPolygonA; + B2DPolygon aTempPolygonB; + temporaryPointVector aTempPointVectorA; + temporaryPointVector aTempPointVectorB; + + // create subdivided polygons and find cuts between them + // Keep adaptiveSubdivideByCount due to needed quality + aTempPolygonA.reserve(SUBDIVIDE_FOR_CUT_TEST_COUNT + 8); + aTempPolygonA.append(rCubicA.getStartPoint()); + rCubicA.adaptiveSubdivideByCount(aTempPolygonA, SUBDIVIDE_FOR_CUT_TEST_COUNT); + aTempPolygonB.reserve(SUBDIVIDE_FOR_CUT_TEST_COUNT + 8); + aTempPolygonB.append(rCubicB.getStartPoint()); + rCubicB.adaptiveSubdivideByCount(aTempPolygonB, SUBDIVIDE_FOR_CUT_TEST_COUNT); + + // #i76891# using findCuts recursively is not sufficient here + findCutsAndTouchesAndCommonForBezier(aTempPolygonA, aTempPolygonB, aTempPointVectorA, aTempPointVectorB); + + if(!aTempPointVectorA.empty()) + { + // adapt tempVector entries to segment + adaptAndTransferCutsWithBezierSegment(aTempPointVectorA, aTempPolygonA, nIndA, rTempPointsA); + } + + if(!aTempPointVectorB.empty()) + { + // adapt tempVector entries to segment + adaptAndTransferCutsWithBezierSegment(aTempPointVectorB, aTempPolygonB, nIndB, rTempPointsB); + } + } + + void findEdgeCutsOneBezier( + const B2DCubicBezier& rCubicA, + sal_uInt32 nInd, temporaryPointVector& rTempPoints) + { + // avoid expensive part of this method if possible + // TODO: use hasAnyExtremum() method instead when it becomes available + double fDummy; + const bool bHasAnyExtremum = rCubicA.getMinimumExtremumPosition( fDummy ); + if( !bHasAnyExtremum ) + return; + + // find all self-intersections on the given bezier segment. Add an entry to the tempPoints + // for each self intersection point with the cut value describing the relative position on given + // bezier segment. + B2DPolygon aTempPolygon; + temporaryPointVector aTempPointVector; + + // create subdivided polygon and find cuts on it + // Keep adaptiveSubdivideByCount due to needed quality + aTempPolygon.reserve(SUBDIVIDE_FOR_CUT_TEST_COUNT + 8); + aTempPolygon.append(rCubicA.getStartPoint()); + rCubicA.adaptiveSubdivideByCount(aTempPolygon, SUBDIVIDE_FOR_CUT_TEST_COUNT); + findCuts(aTempPolygon, aTempPointVector); + + if(!aTempPointVector.empty()) + { + // adapt tempVector entries to segment + adaptAndTransferCutsWithBezierSegment(aTempPointVector, aTempPolygon, nInd, rTempPoints); + } + } + + void findCuts(const B2DPolygon& rCandidate, temporaryPointVector& rTempPoints, size_t* pPointLimit) + { + // find out if there are edges with intersections (self-cuts). If yes, add + // entries to rTempPoints accordingly + const sal_uInt32 nPointCount(rCandidate.count()); + + if(!nPointCount) + return; + + const sal_uInt32 nEdgeCount(rCandidate.isClosed() ? nPointCount : nPointCount - 1); + + if(!nEdgeCount) + return; + + const bool bCurvesInvolved(rCandidate.areControlPointsUsed()); + + if(bCurvesInvolved) + { + B2DCubicBezier aCubicA; + B2DCubicBezier aCubicB; + + for(sal_uInt32 a(0); a < nEdgeCount - 1; a++) + { + rCandidate.getBezierSegment(a, aCubicA); + aCubicA.testAndSolveTrivialBezier(); + const bool bEdgeAIsCurve(aCubicA.isBezier()); + const B2DRange aRangeA(aCubicA.getRange()); + + if(bEdgeAIsCurve) + { + // curved segments may have self-intersections, do not forget those (!) + findEdgeCutsOneBezier(aCubicA, a, rTempPoints); + } + + for(sal_uInt32 b(a + 1); b < nEdgeCount; b++) + { + rCandidate.getBezierSegment(b, aCubicB); + aCubicB.testAndSolveTrivialBezier(); + const B2DRange aRangeB(aCubicB.getRange()); + + // only overlapping segments need to be tested + // consecutive segments touch of course + bool bOverlap = false; + if( b > a+1) + bOverlap = aRangeA.overlaps(aRangeB); + else + bOverlap = aRangeA.overlapsMore(aRangeB); + if( bOverlap) + { + const bool bEdgeBIsCurve(aCubicB.isBezier()); + if(bEdgeAIsCurve && bEdgeBIsCurve) + { + // test for bezier-bezier cuts + findEdgeCutsTwoBeziers(aCubicA, aCubicB, a, b, rTempPoints, rTempPoints); + } + else if(bEdgeAIsCurve) + { + // test for bezier-edge cuts + findEdgeCutsBezierAndEdge(aCubicA, aCubicB.getStartPoint(), aCubicB.getEndPoint(), a, b, rTempPoints, rTempPoints); + } + else if(bEdgeBIsCurve) + { + // test for bezier-edge cuts + findEdgeCutsBezierAndEdge(aCubicB, aCubicA.getStartPoint(), aCubicA.getEndPoint(), b, a, rTempPoints, rTempPoints); + } + else + { + // test for simple edge-edge cuts + findEdgeCutsTwoEdges(aCubicA.getStartPoint(), aCubicA.getEndPoint(), aCubicB.getStartPoint(), aCubicB.getEndPoint(), + a, b, rTempPoints, rTempPoints); + } + } + } + } + } + else + { + B2DPoint aCurrA(rCandidate.getB2DPoint(0)); + + for(sal_uInt32 a(0); a < nEdgeCount - 1; a++) + { + const B2DPoint aNextA(rCandidate.getB2DPoint(a + 1 == nPointCount ? 0 : a + 1)); + const B2DRange aRangeA(aCurrA, aNextA); + B2DPoint aCurrB(rCandidate.getB2DPoint(a + 1)); + + for(sal_uInt32 b(a + 1); b < nEdgeCount; b++) + { + const B2DPoint aNextB(rCandidate.getB2DPoint(b + 1 == nPointCount ? 0 : b + 1)); + const B2DRange aRangeB(aCurrB, aNextB); + + // consecutive segments touch of course + bool bOverlap = false; + if( b > a+1) + bOverlap = aRangeA.overlaps(aRangeB); + else + bOverlap = aRangeA.overlapsMore(aRangeB); + if( bOverlap) + { + findEdgeCutsTwoEdges(aCurrA, aNextA, aCurrB, aNextB, a, b, rTempPoints, rTempPoints); + } + + if (pPointLimit && rTempPoints.size() > *pPointLimit) + break; + + // prepare next step + aCurrB = aNextB; + } + + // prepare next step + aCurrA = aNextA; + } + } + + if (pPointLimit) + { + if (rTempPoints.size() > *pPointLimit) + *pPointLimit = 0; + else + *pPointLimit -= rTempPoints.size(); + } + } + + } // end of anonymous namespace +} // end of namespace basegfx + +namespace basegfx +{ + namespace + { + + void findTouchesOnEdge( + const B2DPoint& rCurr, const B2DPoint& rNext, const B2DPolygon& rPointPolygon, + sal_uInt32 nInd, temporaryPointVector& rTempPoints) + { + // find out if points from rPointPolygon are positioned on given edge. If Yes, add + // points there to represent touches (which may be enter or leave nodes later). + const sal_uInt32 nPointCount(rPointPolygon.count()); + + if(!nPointCount) + return; + + const B2DRange aRange(rCurr, rNext); + const B2DVector aEdgeVector(rNext - rCurr); + bool bTestUsingX(fabs(aEdgeVector.getX()) > fabs(aEdgeVector.getY())); + + for(sal_uInt32 a(0); a < nPointCount; a++) + { + const B2DPoint aTestPoint(rPointPolygon.getB2DPoint(a)); + + if(aRange.isInside(aTestPoint)) + { + if(!aTestPoint.equal(rCurr) && !aTestPoint.equal(rNext)) + { + const B2DVector aTestVector(aTestPoint - rCurr); + + if(areParallel(aEdgeVector, aTestVector)) + { + const double fCut(bTestUsingX + ? aTestVector.getX() / aEdgeVector.getX() + : aTestVector.getY() / aEdgeVector.getY()); + const double fZero(0.0); + const double fOne(1.0); + + if(fTools::more(fCut, fZero) && fTools::less(fCut, fOne)) + { + rTempPoints.emplace_back(aTestPoint, nInd, fCut); + } + } + } + } + } + } + + void findTouchesOnCurve( + const B2DCubicBezier& rCubicA, const B2DPolygon& rPointPolygon, + sal_uInt32 nInd, temporaryPointVector& rTempPoints) + { + // find all points from rPointPolygon which touch the given bezier segment. Add an entry + // for each touch to the given pointVector. The cut for that entry is the relative position on + // the given bezier segment. + B2DPolygon aTempPolygon; + temporaryPointVector aTempPointVector; + + // create subdivided polygon and find cuts on it + // Keep adaptiveSubdivideByCount due to needed quality + aTempPolygon.reserve(SUBDIVIDE_FOR_CUT_TEST_COUNT + 8); + aTempPolygon.append(rCubicA.getStartPoint()); + rCubicA.adaptiveSubdivideByCount(aTempPolygon, SUBDIVIDE_FOR_CUT_TEST_COUNT); + findTouches(aTempPolygon, rPointPolygon, aTempPointVector); + + if(!aTempPointVector.empty()) + { + // adapt tempVector entries to segment + adaptAndTransferCutsWithBezierSegment(aTempPointVector, aTempPolygon, nInd, rTempPoints); + } + } + + void findTouches(const B2DPolygon& rEdgePolygon, const B2DPolygon& rPointPolygon, temporaryPointVector& rTempPoints) + { + // find out if points from rPointPolygon touch edges from rEdgePolygon. If yes, + // add entries to rTempPoints + const sal_uInt32 nPointCount(rPointPolygon.count()); + const sal_uInt32 nEdgePointCount(rEdgePolygon.count()); + + if(!(nPointCount && nEdgePointCount)) + return; + + const sal_uInt32 nEdgeCount(rEdgePolygon.isClosed() ? nEdgePointCount : nEdgePointCount - 1); + B2DPoint aCurr(rEdgePolygon.getB2DPoint(0)); + + for(sal_uInt32 a(0); a < nEdgeCount; a++) + { + const sal_uInt32 nNextIndex((a + 1) % nEdgePointCount); + const B2DPoint aNext(rEdgePolygon.getB2DPoint(nNextIndex)); + + if(!aCurr.equal(aNext)) + { + bool bHandleAsSimpleEdge(true); + + if(rEdgePolygon.areControlPointsUsed()) + { + const B2DPoint aNextControlPoint(rEdgePolygon.getNextControlPoint(a)); + const B2DPoint aPrevControlPoint(rEdgePolygon.getPrevControlPoint(nNextIndex)); + const bool bEdgeIsCurve(!aNextControlPoint.equal(aCurr) || !aPrevControlPoint.equal(aNext)); + + if(bEdgeIsCurve) + { + bHandleAsSimpleEdge = false; + const B2DCubicBezier aCubicA(aCurr, aNextControlPoint, aPrevControlPoint, aNext); + findTouchesOnCurve(aCubicA, rPointPolygon, a, rTempPoints); + } + } + + if(bHandleAsSimpleEdge) + { + findTouchesOnEdge(aCurr, aNext, rPointPolygon, a, rTempPoints); + } + } + + // next step + aCurr = aNext; + } + } + + } // end of anonymous namespace +} // end of namespace basegfx + +namespace basegfx +{ + namespace + { + + void findCuts(const B2DPolygon& rCandidateA, const B2DPolygon& rCandidateB, temporaryPointVector& rTempPointsA, temporaryPointVector& rTempPointsB) + { + // find out if edges from both polygons cut. If so, add entries to rTempPoints which + // should be added to the polygons accordingly + const sal_uInt32 nPointCountA(rCandidateA.count()); + const sal_uInt32 nPointCountB(rCandidateB.count()); + + if(!(nPointCountA && nPointCountB)) + return; + + const sal_uInt32 nEdgeCountA(rCandidateA.isClosed() ? nPointCountA : nPointCountA - 1); + const sal_uInt32 nEdgeCountB(rCandidateB.isClosed() ? nPointCountB : nPointCountB - 1); + + if(!(nEdgeCountA && nEdgeCountB)) + return; + + const bool bCurvesInvolved(rCandidateA.areControlPointsUsed() || rCandidateB.areControlPointsUsed()); + + if(bCurvesInvolved) + { + B2DCubicBezier aCubicA; + B2DCubicBezier aCubicB; + + for(sal_uInt32 a(0); a < nEdgeCountA; a++) + { + rCandidateA.getBezierSegment(a, aCubicA); + aCubicA.testAndSolveTrivialBezier(); + const bool bEdgeAIsCurve(aCubicA.isBezier()); + const B2DRange aRangeA(aCubicA.getRange()); + + for(sal_uInt32 b(0); b < nEdgeCountB; b++) + { + rCandidateB.getBezierSegment(b, aCubicB); + aCubicB.testAndSolveTrivialBezier(); + const B2DRange aRangeB(aCubicB.getRange()); + + // consecutive segments touch of course + bool bOverlap = false; + if( b > a+1) + bOverlap = aRangeA.overlaps(aRangeB); + else + bOverlap = aRangeA.overlapsMore(aRangeB); + if( bOverlap) + { + const bool bEdgeBIsCurve(aCubicB.isBezier()); + if(bEdgeAIsCurve && bEdgeBIsCurve) + { + // test for bezier-bezier cuts + findEdgeCutsTwoBeziers(aCubicA, aCubicB, a, b, rTempPointsA, rTempPointsB); + } + else if(bEdgeAIsCurve) + { + // test for bezier-edge cuts + findEdgeCutsBezierAndEdge(aCubicA, aCubicB.getStartPoint(), aCubicB.getEndPoint(), a, b, rTempPointsA, rTempPointsB); + } + else if(bEdgeBIsCurve) + { + // test for bezier-edge cuts + findEdgeCutsBezierAndEdge(aCubicB, aCubicA.getStartPoint(), aCubicA.getEndPoint(), b, a, rTempPointsB, rTempPointsA); + } + else + { + // test for simple edge-edge cuts + findEdgeCutsTwoEdges(aCubicA.getStartPoint(), aCubicA.getEndPoint(), aCubicB.getStartPoint(), aCubicB.getEndPoint(), + a, b, rTempPointsA, rTempPointsB); + } + } + } + } + } + else + { + B2DPoint aCurrA(rCandidateA.getB2DPoint(0)); + + for(sal_uInt32 a(0); a < nEdgeCountA; a++) + { + const B2DPoint aNextA(rCandidateA.getB2DPoint(a + 1 == nPointCountA ? 0 : a + 1)); + const B2DRange aRangeA(aCurrA, aNextA); + B2DPoint aCurrB(rCandidateB.getB2DPoint(0)); + + for(sal_uInt32 b(0); b < nEdgeCountB; b++) + { + const B2DPoint aNextB(rCandidateB.getB2DPoint(b + 1 == nPointCountB ? 0 : b + 1)); + const B2DRange aRangeB(aCurrB, aNextB); + + // consecutive segments touch of course + bool bOverlap = false; + if( b > a+1) + bOverlap = aRangeA.overlaps(aRangeB); + else + bOverlap = aRangeA.overlapsMore(aRangeB); + if( bOverlap) + { + // test for simple edge-edge cuts + findEdgeCutsTwoEdges(aCurrA, aNextA, aCurrB, aNextB, a, b, rTempPointsA, rTempPointsB); + } + + // prepare next step + aCurrB = aNextB; + } + + // prepare next step + aCurrA = aNextA; + } + } + } + + } // end of anonymous namespace +} // end of namespace basegfx + +namespace basegfx::utils +{ + + B2DPolygon addPointsAtCutsAndTouches(const B2DPolygon& rCandidate, size_t* pPointLimit) + { + if(rCandidate.count()) + { + temporaryPointVector aTempPoints; + + findTouches(rCandidate, rCandidate, aTempPoints); + findCuts(rCandidate, aTempPoints, pPointLimit); + if (pPointLimit && !*pPointLimit) + { + SAL_WARN("basegfx", "addPointsAtCutsAndTouches hit point limit"); + return rCandidate; + } + + return mergeTemporaryPointsAndPolygon(rCandidate, aTempPoints); + } + else + { + return rCandidate; + } + } + + B2DPolyPolygon addPointsAtCutsAndTouches(const B2DPolyPolygon& rCandidate, size_t* pPointLimit) + { + const sal_uInt32 nCount(rCandidate.count()); + + if(nCount) + { + B2DPolyPolygon aRetval; + + if(nCount == 1) + { + // remove self intersections + aRetval.append(addPointsAtCutsAndTouches(rCandidate.getB2DPolygon(0), pPointLimit)); + } + else + { + // first solve self cuts and self touches for all contained single polygons + std::unique_ptr<temporaryPolygonData[]> pTempData(new temporaryPolygonData[nCount]); + sal_uInt32 a, b; + + for(a = 0; a < nCount; a++) + { + // use polygons with solved self intersections + pTempData[a].setPolygon(addPointsAtCutsAndTouches(rCandidate.getB2DPolygon(a), pPointLimit)); + } + + if (pPointLimit && !*pPointLimit) + { + SAL_WARN("basegfx", "addPointsAtCutsAndTouches hit point limit"); + return rCandidate; + } + + // now cuts and touches between the polygons + for(a = 0; a < nCount; a++) + { + for(b = 0; b < nCount; b++) + { + if(a != b) + { + // look for touches, compare each edge polygon to all other points + if(pTempData[a].getRange().overlaps(pTempData[b].getRange())) + { + findTouches(pTempData[a].getPolygon(), pTempData[b].getPolygon(), pTempData[a].getTemporaryPointVector()); + } + } + + if(a < b) + { + // look for cuts, compare each edge polygon to following ones + if(pTempData[a].getRange().overlaps(pTempData[b].getRange())) + { + findCuts(pTempData[a].getPolygon(), pTempData[b].getPolygon(), pTempData[a].getTemporaryPointVector(), pTempData[b].getTemporaryPointVector()); + } + } + } + } + + // consolidate the result + for(a = 0; a < nCount; a++) + { + aRetval.append(mergeTemporaryPointsAndPolygon(pTempData[a].getPolygon(), pTempData[a].getTemporaryPointVector())); + } + } + + return aRetval; + } + else + { + return rCandidate; + } + } + + B2DPolygon addPointsAtCuts(const B2DPolygon& rCandidate, const B2DPoint& rStart, const B2DPoint& rEnd) + { + const sal_uInt32 nCount(rCandidate.count()); + + if(nCount && !rStart.equal(rEnd)) + { + const B2DRange aPolygonRange(rCandidate.getB2DRange()); + const B2DRange aEdgeRange(rStart, rEnd); + + if(aPolygonRange.overlaps(aEdgeRange)) + { + const sal_uInt32 nEdgeCount(rCandidate.isClosed() ? nCount : nCount - 1); + temporaryPointVector aTempPoints; + temporaryPointVector aUnusedTempPoints; + B2DCubicBezier aCubic; + + for(sal_uInt32 a(0); a < nEdgeCount; a++) + { + rCandidate.getBezierSegment(a, aCubic); + B2DRange aCubicRange(aCubic.getStartPoint(), aCubic.getEndPoint()); + + if(aCubic.isBezier()) + { + aCubicRange.expand(aCubic.getControlPointA()); + aCubicRange.expand(aCubic.getControlPointB()); + + if(aCubicRange.overlaps(aEdgeRange)) + { + findEdgeCutsBezierAndEdge(aCubic, rStart, rEnd, a, 0, aTempPoints, aUnusedTempPoints); + } + } + else + { + if(aCubicRange.overlaps(aEdgeRange)) + { + findEdgeCutsTwoEdges(aCubic.getStartPoint(), aCubic.getEndPoint(), rStart, rEnd, a, 0, aTempPoints, aUnusedTempPoints); + } + } + } + + return mergeTemporaryPointsAndPolygon(rCandidate, aTempPoints); + } + } + + return rCandidate; + } + + B2DPolygon addPointsAtCuts(const B2DPolygon& rCandidate, const B2DPolyPolygon& rPolyMask) + { + const sal_uInt32 nCountA(rCandidate.count()); + const sal_uInt32 nCountM(rPolyMask.count()); + + if(nCountA && nCountM) + { + const B2DRange aRangeA(rCandidate.getB2DRange()); + const B2DRange aRangeM(rPolyMask.getB2DRange()); + + if(aRangeA.overlaps(aRangeM)) + { + const sal_uInt32 nEdgeCountA(rCandidate.isClosed() ? nCountA : nCountA - 1); + temporaryPointVector aTempPointsA; + temporaryPointVector aUnusedTempPointsB; + + for(sal_uInt32 m(0); m < nCountM; m++) + { + const B2DPolygon& aMask(rPolyMask.getB2DPolygon(m)); + const sal_uInt32 nCountB(aMask.count()); + + if(nCountB) + { + B2DCubicBezier aCubicA; + B2DCubicBezier aCubicB; + + for(sal_uInt32 a(0); a < nEdgeCountA; a++) + { + rCandidate.getBezierSegment(a, aCubicA); + const bool bCubicAIsCurve(aCubicA.isBezier()); + B2DRange aCubicRangeA(aCubicA.getStartPoint(), aCubicA.getEndPoint()); + + if(bCubicAIsCurve) + { + aCubicRangeA.expand(aCubicA.getControlPointA()); + aCubicRangeA.expand(aCubicA.getControlPointB()); + } + + for(sal_uInt32 b(0); b < nCountB; b++) + { + aMask.getBezierSegment(b, aCubicB); + const bool bCubicBIsCurve(aCubicB.isBezier()); + B2DRange aCubicRangeB(aCubicB.getStartPoint(), aCubicB.getEndPoint()); + + if(bCubicBIsCurve) + { + aCubicRangeB.expand(aCubicB.getControlPointA()); + aCubicRangeB.expand(aCubicB.getControlPointB()); + } + + if(aCubicRangeA.overlaps(aCubicRangeB)) + { + if(bCubicAIsCurve && bCubicBIsCurve) + { + findEdgeCutsTwoBeziers(aCubicA, aCubicB, a, b, aTempPointsA, aUnusedTempPointsB); + } + else if(bCubicAIsCurve) + { + findEdgeCutsBezierAndEdge(aCubicA, aCubicB.getStartPoint(), aCubicB.getEndPoint(), a, b, aTempPointsA, aUnusedTempPointsB); + } + else if(bCubicBIsCurve) + { + findEdgeCutsBezierAndEdge(aCubicB, aCubicA.getStartPoint(), aCubicA.getEndPoint(), b, a, aUnusedTempPointsB, aTempPointsA); + } + else + { + findEdgeCutsTwoEdges(aCubicA.getStartPoint(), aCubicA.getEndPoint(), aCubicB.getStartPoint(), aCubicB.getEndPoint(), a, b, aTempPointsA, aUnusedTempPointsB); + } + } + } + } + } + } + + return mergeTemporaryPointsAndPolygon(rCandidate, aTempPointsA); + } + } + + return rCandidate; + } + +} // end of namespace + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/basegfx/source/polygon/b2dpolygontools.cxx b/basegfx/source/polygon/b2dpolygontools.cxx new file mode 100644 index 0000000000..b3f43669dd --- /dev/null +++ b/basegfx/source/polygon/b2dpolygontools.cxx @@ -0,0 +1,3702 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * 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 . + */ +#include <numeric> +#include <algorithm> +#include <stack> + +#include <basegfx/numeric/ftools.hxx> +#include <basegfx/polygon/b2dpolygontools.hxx> +#include <osl/diagnose.h> +#include <rtl/math.hxx> +#include <sal/log.hxx> +#include <basegfx/polygon/b2dpolygon.hxx> +#include <basegfx/polygon/b2dpolypolygon.hxx> +#include <basegfx/range/b2drange.hxx> +#include <basegfx/curve/b2dcubicbezier.hxx> +#include <basegfx/point/b3dpoint.hxx> +#include <basegfx/matrix/b3dhommatrix.hxx> +#include <basegfx/matrix/b2dhommatrix.hxx> +#include <basegfx/curve/b2dbeziertools.hxx> +#include <basegfx/matrix/b2dhommatrixtools.hxx> + +// #i37443# +#define ANGLE_BOUND_START_VALUE (2.25) +#define ANGLE_BOUND_MINIMUM_VALUE (0.1) +#define STEPSPERQUARTER (3) + +namespace basegfx::utils +{ + void openWithGeometryChange(B2DPolygon& rCandidate) + { + if(!rCandidate.isClosed()) + return; + + if(rCandidate.count()) + { + rCandidate.append(rCandidate.getB2DPoint(0)); + + if(rCandidate.areControlPointsUsed() && rCandidate.isPrevControlPointUsed(0)) + { + rCandidate.setPrevControlPoint(rCandidate.count() - 1, rCandidate.getPrevControlPoint(0)); + rCandidate.resetPrevControlPoint(0); + } + } + + rCandidate.setClosed(false); + } + + void closeWithGeometryChange(B2DPolygon& rCandidate) + { + if(rCandidate.isClosed()) + return; + + while(rCandidate.count() > 1 && rCandidate.getB2DPoint(0) == rCandidate.getB2DPoint(rCandidate.count() - 1)) + { + if(rCandidate.areControlPointsUsed() && rCandidate.isPrevControlPointUsed(rCandidate.count() - 1)) + { + rCandidate.setPrevControlPoint(0, rCandidate.getPrevControlPoint(rCandidate.count() - 1)); + } + + rCandidate.remove(rCandidate.count() - 1); + } + + rCandidate.setClosed(true); + } + + void checkClosed(B2DPolygon& rCandidate) + { + // #i80172# Removed unnecessary assertion + // OSL_ENSURE(!rCandidate.isClosed(), "checkClosed: already closed (!)"); + + if(rCandidate.count() > 1 && rCandidate.getB2DPoint(0) == rCandidate.getB2DPoint(rCandidate.count() - 1)) + { + closeWithGeometryChange(rCandidate); + } + } + + // Get successor and predecessor indices. Returning the same index means there + // is none. Same for successor. + sal_uInt32 getIndexOfPredecessor(sal_uInt32 nIndex, const B2DPolygon& rCandidate) + { + OSL_ENSURE(nIndex < rCandidate.count(), "getIndexOfPredecessor: Access to polygon out of range (!)"); + + if(nIndex) + { + return nIndex - 1; + } + else if(rCandidate.count()) + { + return rCandidate.count() - 1; + } + else + { + return nIndex; + } + } + + sal_uInt32 getIndexOfSuccessor(sal_uInt32 nIndex, const B2DPolygon& rCandidate) + { + OSL_ENSURE(nIndex < rCandidate.count(), "getIndexOfPredecessor: Access to polygon out of range (!)"); + + if(nIndex + 1 < rCandidate.count()) + { + return nIndex + 1; + } + else if(nIndex + 1 == rCandidate.count()) + { + return 0; + } + else + { + return nIndex; + } + } + + B2VectorOrientation getOrientation(const B2DPolygon& rCandidate) + { + B2VectorOrientation eRetval(B2VectorOrientation::Neutral); + + if(rCandidate.count() > 2 || rCandidate.areControlPointsUsed()) + { + const double fSignedArea(getSignedArea(rCandidate)); + + if(fTools::equalZero(fSignedArea)) + { + // B2VectorOrientation::Neutral, already set + } + if(fSignedArea > 0.0) + { + eRetval = B2VectorOrientation::Positive; + } + else if(fSignedArea < 0.0) + { + eRetval = B2VectorOrientation::Negative; + } + } + + return eRetval; + } + + B2VectorContinuity getContinuityInPoint(const B2DPolygon& rCandidate, sal_uInt32 nIndex) + { + return rCandidate.getContinuityInPoint(nIndex); + } + + B2DPolygon adaptiveSubdivideByDistance(const B2DPolygon& rCandidate, double fDistanceBound, int nRecurseLimit) + { + if(rCandidate.areControlPointsUsed()) + { + const sal_uInt32 nPointCount(rCandidate.count()); + B2DPolygon aRetval; + + if(nPointCount) + { + // prepare edge-oriented loop + const sal_uInt32 nEdgeCount(rCandidate.isClosed() ? nPointCount : nPointCount - 1); + B2DCubicBezier aBezier; + aBezier.setStartPoint(rCandidate.getB2DPoint(0)); + + // perf: try to avoid too many reallocations by guessing the result's pointcount + aRetval.reserve(nPointCount*4); + + // add start point (always) + aRetval.append(aBezier.getStartPoint()); + + for(sal_uInt32 a(0); a < nEdgeCount; a++) + { + // get next and control points + const sal_uInt32 nNextIndex((a + 1) % nPointCount); + aBezier.setEndPoint(rCandidate.getB2DPoint(nNextIndex)); + aBezier.setControlPointA(rCandidate.getNextControlPoint(a)); + aBezier.setControlPointB(rCandidate.getPrevControlPoint(nNextIndex)); + aBezier.testAndSolveTrivialBezier(); + + if(aBezier.isBezier()) + { + // add curved edge and generate DistanceBound + double fBound(0.0); + + if(fDistanceBound == 0.0) + { + // If not set, use B2DCubicBezier functionality to guess a rough value + const double fRoughLength((aBezier.getEdgeLength() + aBezier.getControlPolygonLength()) / 2.0); + + // take 1/100th of the rough curve length + fBound = fRoughLength * 0.01; + } + else + { + // use given bound value + fBound = fDistanceBound; + } + + // make sure bound value is not too small. The base units are 1/100th mm, thus + // just make sure it's not smaller then 1/100th of that + if(fBound < 0.01) + { + fBound = 0.01; + } + + // call adaptive subdivide which adds edges to aRetval accordingly + aBezier.adaptiveSubdivideByDistance(aRetval, fBound, nRecurseLimit); + } + else + { + // add non-curved edge + aRetval.append(aBezier.getEndPoint()); + } + + // prepare next step + aBezier.setStartPoint(aBezier.getEndPoint()); + } + + if(rCandidate.isClosed()) + { + // set closed flag and correct last point (which is added double now). + closeWithGeometryChange(aRetval); + } + } + + return aRetval; + } + else + { + return rCandidate; + } + } + + B2DPolygon adaptiveSubdivideByAngle(const B2DPolygon& rCandidate, double fAngleBound) + { + if(rCandidate.areControlPointsUsed()) + { + const sal_uInt32 nPointCount(rCandidate.count()); + B2DPolygon aRetval; + + if(nPointCount) + { + // prepare edge-oriented loop + const sal_uInt32 nEdgeCount(rCandidate.isClosed() ? nPointCount : nPointCount - 1); + B2DCubicBezier aBezier; + aBezier.setStartPoint(rCandidate.getB2DPoint(0)); + + // perf: try to avoid too many reallocations by guessing the result's pointcount + aRetval.reserve(nPointCount*4); + + // add start point (always) + aRetval.append(aBezier.getStartPoint()); + + // #i37443# prepare convenient AngleBound if none was given + if(fAngleBound == 0.0) + { + fAngleBound = ANGLE_BOUND_START_VALUE; + } + else if(fTools::less(fAngleBound, ANGLE_BOUND_MINIMUM_VALUE)) + { + fAngleBound = 0.1; + } + + for(sal_uInt32 a(0); a < nEdgeCount; a++) + { + // get next and control points + const sal_uInt32 nNextIndex((a + 1) % nPointCount); + aBezier.setEndPoint(rCandidate.getB2DPoint(nNextIndex)); + aBezier.setControlPointA(rCandidate.getNextControlPoint(a)); + aBezier.setControlPointB(rCandidate.getPrevControlPoint(nNextIndex)); + aBezier.testAndSolveTrivialBezier(); + + if(aBezier.isBezier()) + { + // call adaptive subdivide + aBezier.adaptiveSubdivideByAngle(aRetval, fAngleBound); + } + else + { + // add non-curved edge + aRetval.append(aBezier.getEndPoint()); + } + + // prepare next step + aBezier.setStartPoint(aBezier.getEndPoint()); + } + + if(rCandidate.isClosed()) + { + // set closed flag and correct last point (which is added double now). + closeWithGeometryChange(aRetval); + } + } + + return aRetval; + } + else + { + return rCandidate; + } + } + + bool isInside(const B2DPolygon& rCandidate, const B2DPoint& rPoint, bool bWithBorder) + { + const B2DPolygon aCandidate(rCandidate.areControlPointsUsed() ? rCandidate.getDefaultAdaptiveSubdivision() : rCandidate); + + if(bWithBorder && isPointOnPolygon(aCandidate, rPoint)) + { + return true; + } + else + { + bool bRetval(false); + const sal_uInt32 nPointCount(aCandidate.count()); + + if(nPointCount) + { + B2DPoint aCurrentPoint(aCandidate.getB2DPoint(nPointCount - 1)); + + for(sal_uInt32 a(0); a < nPointCount; a++) + { + const B2DPoint aPreviousPoint(aCurrentPoint); + aCurrentPoint = aCandidate.getB2DPoint(a); + + // cross-over in Y? tdf#130150 use full precision, no need for epsilon + const bool bCompYA(aPreviousPoint.getY() > rPoint.getY()); + const bool bCompYB(aCurrentPoint.getY() > rPoint.getY()); + + if(bCompYA != bCompYB) + { + // cross-over in X? tdf#130150 use full precision, no need for epsilon + const bool bCompXA(aPreviousPoint.getX() > rPoint.getX()); + const bool bCompXB(aCurrentPoint.getX() > rPoint.getX()); + + if(bCompXA == bCompXB) + { + if(bCompXA) + { + bRetval = !bRetval; + } + } + else + { + const double fCompare( + aCurrentPoint.getX() - (aCurrentPoint.getY() - rPoint.getY()) * + (aPreviousPoint.getX() - aCurrentPoint.getX()) / + (aPreviousPoint.getY() - aCurrentPoint.getY())); + + // tdf#130150 use full precision, no need for epsilon + if(fCompare > rPoint.getX()) + { + bRetval = !bRetval; + } + } + } + } + } + + return bRetval; + } + } + + bool isInside(const B2DPolygon& rCandidate, const B2DPolygon& rPolygon, bool bWithBorder) + { + const B2DPolygon aCandidate(rCandidate.areControlPointsUsed() ? rCandidate.getDefaultAdaptiveSubdivision() : rCandidate); + const B2DPolygon aPolygon(rPolygon.areControlPointsUsed() ? rPolygon.getDefaultAdaptiveSubdivision() : rPolygon); + const sal_uInt32 nPointCount(aPolygon.count()); + + for(sal_uInt32 a(0); a < nPointCount; a++) + { + const B2DPoint aTestPoint(aPolygon.getB2DPoint(a)); + + if(!isInside(aCandidate, aTestPoint, bWithBorder)) + { + return false; + } + } + + return true; + } + + B2DRange getRange(const B2DPolygon& rCandidate) + { + // changed to use internally buffered version at B2DPolygon + return rCandidate.getB2DRange(); + } + + double getSignedArea(const B2DPolygon& rCandidate) + { + const B2DPolygon aCandidate(rCandidate.areControlPointsUsed() ? rCandidate.getDefaultAdaptiveSubdivision() : rCandidate); + double fRetval(0.0); + const sal_uInt32 nPointCount(aCandidate.count()); + + if(nPointCount > 2) + { + for(sal_uInt32 a(0); a < nPointCount; a++) + { + const B2DPoint aPreviousPoint(aCandidate.getB2DPoint((!a) ? nPointCount - 1 : a - 1)); + const B2DPoint aCurrentPoint(aCandidate.getB2DPoint(a)); + + fRetval += aPreviousPoint.getX() * aCurrentPoint.getY(); + fRetval -= aPreviousPoint.getY() * aCurrentPoint.getX(); + } + + // correct to zero if small enough. Also test the quadratic + // of the result since the precision is near quadratic due to + // the algorithm + if(fTools::equalZero(fRetval) || fTools::equalZero(fRetval * fRetval)) + { + fRetval = 0.0; + } + } + + return fRetval; + } + + double getArea(const B2DPolygon& rCandidate) + { + double fRetval(0.0); + + if(rCandidate.count() > 2 || rCandidate.areControlPointsUsed()) + { + fRetval = getSignedArea(rCandidate); + const double fZero(0.0); + + if(fTools::less(fRetval, fZero)) + { + fRetval = -fRetval; + } + } + + return fRetval; + } + + double getEdgeLength(const B2DPolygon& rCandidate, sal_uInt32 nIndex) + { + const sal_uInt32 nPointCount(rCandidate.count()); + OSL_ENSURE(nIndex < nPointCount, "getEdgeLength: Access to polygon out of range (!)"); + double fRetval(0.0); + + if(nPointCount) + { + const sal_uInt32 nNextIndex((nIndex + 1) % nPointCount); + + if(rCandidate.areControlPointsUsed()) + { + B2DCubicBezier aEdge; + + aEdge.setStartPoint(rCandidate.getB2DPoint(nIndex)); + aEdge.setControlPointA(rCandidate.getNextControlPoint(nIndex)); + aEdge.setControlPointB(rCandidate.getPrevControlPoint(nNextIndex)); + aEdge.setEndPoint(rCandidate.getB2DPoint(nNextIndex)); + + fRetval = aEdge.getLength(); + } + else + { + const B2DPoint aCurrent(rCandidate.getB2DPoint(nIndex)); + const B2DPoint aNext(rCandidate.getB2DPoint(nNextIndex)); + + fRetval = B2DVector(aNext - aCurrent).getLength(); + } + } + + return fRetval; + } + + double getLength(const B2DPolygon& rCandidate, bool bApproximateBezierLength) + { + double fRetval(0.0); + const sal_uInt32 nPointCount(rCandidate.count()); + + if(nPointCount) + { + const sal_uInt32 nEdgeCount(rCandidate.isClosed() ? nPointCount : nPointCount - 1); + + if(rCandidate.areControlPointsUsed()) + { + if (bApproximateBezierLength) + { + B2DPoint aStartPoint = rCandidate.getB2DPoint(0); + + for(sal_uInt32 a(0); a < nEdgeCount; a++) + { + // An approximate length of a cubic Bezier curve is the average + // of its chord length and the sum of the lengths of its control net sides. + const sal_uInt32 nNextIndex((a + 1) % nPointCount); + const B2DPoint& aControlPoint1 = rCandidate.getNextControlPoint(a); + const B2DPoint& aControlPoint2 = rCandidate.getPrevControlPoint(nNextIndex); + const B2DPoint& aEndPoint = rCandidate.getB2DPoint(nNextIndex); + + double chord_length = B2DVector(aEndPoint - aStartPoint).getLength(); + double control_net_length = B2DVector(aStartPoint - aControlPoint1).getLength() + + B2DVector(aControlPoint2 - aControlPoint1).getLength() + + B2DVector(aEndPoint - aControlPoint2).getLength(); + double approximate_arc_length = (control_net_length + chord_length) / 2; + + fRetval += approximate_arc_length; + aStartPoint = aEndPoint; + } + + } + else + { + B2DCubicBezier aEdge; + aEdge.setStartPoint(rCandidate.getB2DPoint(0)); + + for(sal_uInt32 a(0); a < nEdgeCount; a++) + { + const sal_uInt32 nNextIndex((a + 1) % nPointCount); + aEdge.setControlPointA(rCandidate.getNextControlPoint(a)); + aEdge.setControlPointB(rCandidate.getPrevControlPoint(nNextIndex)); + aEdge.setEndPoint(rCandidate.getB2DPoint(nNextIndex)); + + fRetval += aEdge.getLength(); + aEdge.setStartPoint(aEdge.getEndPoint()); + } + } + } + else + { + B2DPoint aCurrent(rCandidate.getB2DPoint(0)); + + for(sal_uInt32 a(0); a < nEdgeCount; a++) + { + const sal_uInt32 nNextIndex((a + 1) % nPointCount); + const B2DPoint aNext(rCandidate.getB2DPoint(nNextIndex)); + + fRetval += B2DVector(aNext - aCurrent).getLength(); + aCurrent = aNext; + } + } + } + + return fRetval; + } + + B2DPoint getPositionAbsolute(const B2DPolygon& rCandidate, double fDistance, double fLength) + { + B2DPoint aRetval; + const sal_uInt32 nPointCount(rCandidate.count()); + + if( nPointCount == 1 ) + { + // only one point (i.e. no edge) - simply take that point + aRetval = rCandidate.getB2DPoint(0); + } + else if(nPointCount > 1) + { + const sal_uInt32 nEdgeCount(rCandidate.isClosed() ? nPointCount : nPointCount - 1); + sal_uInt32 nIndex(0); + bool bIndexDone(false); + + // get length if not given + if(fTools::equalZero(fLength)) + { + fLength = getLength(rCandidate); + } + + if(fTools::less(fDistance, 0.0)) + { + // handle fDistance < 0.0 + if(rCandidate.isClosed()) + { + // if fDistance < 0.0 increment with multiple of fLength + sal_uInt32 nCount(sal_uInt32(-fDistance / fLength)); + fDistance += double(nCount + 1) * fLength; + } + else + { + // crop to polygon start + fDistance = 0.0; + bIndexDone = true; + } + } + else if(fTools::moreOrEqual(fDistance, fLength)) + { + // handle fDistance >= fLength + if(rCandidate.isClosed()) + { + // if fDistance >= fLength decrement with multiple of fLength + sal_uInt32 nCount(sal_uInt32(fDistance / fLength)); + fDistance -= static_cast<double>(nCount) * fLength; + } + else + { + // crop to polygon end + fDistance = 0.0; + nIndex = nEdgeCount; + bIndexDone = true; + } + } + + // look for correct index. fDistance is now [0.0 .. fLength[ + double fEdgeLength(getEdgeLength(rCandidate, nIndex)); + + while(!bIndexDone) + { + // edge found must be on the half-open range + // [0,fEdgeLength). + // Note that in theory, we cannot move beyond + // the last polygon point, since fDistance>=fLength + // is checked above. Unfortunately, with floating- + // point calculations, this case might happen. + // Handled by nIndex check below + if (nIndex+1 < nEdgeCount && fTools::moreOrEqual(fDistance, fEdgeLength)) + { + // go to next edge + fDistance -= fEdgeLength; + fEdgeLength = getEdgeLength(rCandidate, ++nIndex); + } + else + { + // it's on this edge, stop + bIndexDone = true; + } + } + + // get the point using nIndex + aRetval = rCandidate.getB2DPoint(nIndex); + + // if fDistance != 0.0, move that length on the edge. The edge + // length is in fEdgeLength. + if(!fTools::equalZero(fDistance)) + { + if(fTools::moreOrEqual(fDistance, fEdgeLength)) + { + // end point of chosen edge + const sal_uInt32 nNextIndex((nIndex + 1) % nPointCount); + aRetval = rCandidate.getB2DPoint(nNextIndex); + } + else if(fTools::equalZero(fDistance)) + { + // start point of chosen edge + } + else + { + // inside edge + const sal_uInt32 nNextIndex((nIndex + 1) % nPointCount); + const B2DPoint aNextPoint(rCandidate.getB2DPoint(nNextIndex)); + bool bDone(false); + + // add calculated average value to the return value + if(rCandidate.areControlPointsUsed()) + { + // get as bezier segment + const B2DCubicBezier aBezierSegment( + aRetval, rCandidate.getNextControlPoint(nIndex), + rCandidate.getPrevControlPoint(nNextIndex), aNextPoint); + + if(aBezierSegment.isBezier()) + { + // use B2DCubicBezierHelper to bridge the non-linear gap between + // length and bezier distances + const B2DCubicBezierHelper aBezierSegmentHelper(aBezierSegment); + const double fBezierDistance(aBezierSegmentHelper.distanceToRelative(fDistance)); + + aRetval = aBezierSegment.interpolatePoint(fBezierDistance); + bDone = true; + } + } + + if(!bDone) + { + const double fRelativeInEdge(fDistance / fEdgeLength); + aRetval = interpolate(aRetval, aNextPoint, fRelativeInEdge); + } + } + } + } + + return aRetval; + } + + B2DPoint getPositionRelative(const B2DPolygon& rCandidate, double fDistance, double fLength) + { + // get length if not given + if(fTools::equalZero(fLength)) + { + fLength = getLength(rCandidate); + } + + // multiply fDistance with real length to get absolute position and + // use getPositionAbsolute + return getPositionAbsolute(rCandidate, fDistance * fLength, fLength); + } + + B2DPolygon getSnippetAbsolute(const B2DPolygon& rCandidate, double fFrom, double fTo, double fLength) + { + const sal_uInt32 nPointCount(rCandidate.count()); + + if(nPointCount) + { + // get length if not given + if(fTools::equalZero(fLength)) + { + fLength = getLength(rCandidate); + } + + // test and correct fFrom + if(fTools::less(fFrom, 0.0)) + { + fFrom = 0.0; + } + + // test and correct fTo + if(fTools::more(fTo, fLength)) + { + fTo = fLength; + } + + // test and correct relationship of fFrom, fTo + if(fTools::more(fFrom, fTo)) + { + fFrom = fTo = (fFrom + fTo) / 2.0; + } + + if(fTools::equalZero(fFrom) && fTools::equal(fTo, fLength)) + { + // no change, result is the whole polygon + return rCandidate; + } + else + { + B2DPolygon aRetval; + const sal_uInt32 nEdgeCount(rCandidate.isClosed() ? nPointCount : nPointCount - 1); + double fPositionOfStart(0.0); + bool bStartDone(false); + bool bEndDone(false); + + for(sal_uInt32 a(0); !(bStartDone && bEndDone) && a < nEdgeCount; a++) + { + const double fEdgeLength(getEdgeLength(rCandidate, a)); + + if(!bStartDone) + { + if(fTools::equalZero(fFrom)) + { + aRetval.append(rCandidate.getB2DPoint(a)); + + if(rCandidate.areControlPointsUsed()) + { + aRetval.setNextControlPoint(aRetval.count() - 1, rCandidate.getNextControlPoint(a)); + } + + bStartDone = true; + } + else if(fTools::moreOrEqual(fFrom, fPositionOfStart) && fTools::less(fFrom, fPositionOfStart + fEdgeLength)) + { + // calculate and add start point + if(fTools::equalZero(fEdgeLength)) + { + aRetval.append(rCandidate.getB2DPoint(a)); + + if(rCandidate.areControlPointsUsed()) + { + aRetval.setNextControlPoint(aRetval.count() - 1, rCandidate.getNextControlPoint(a)); + } + } + else + { + const sal_uInt32 nNextIndex((a + 1) % nPointCount); + const B2DPoint aStart(rCandidate.getB2DPoint(a)); + const B2DPoint aEnd(rCandidate.getB2DPoint(nNextIndex)); + bool bDone(false); + + if(rCandidate.areControlPointsUsed()) + { + const B2DCubicBezier aBezierSegment( + aStart, rCandidate.getNextControlPoint(a), + rCandidate.getPrevControlPoint(nNextIndex), aEnd); + + if(aBezierSegment.isBezier()) + { + // use B2DCubicBezierHelper to bridge the non-linear gap between + // length and bezier distances + const B2DCubicBezierHelper aBezierSegmentHelper(aBezierSegment); + const double fBezierDistance(aBezierSegmentHelper.distanceToRelative(fFrom - fPositionOfStart)); + B2DCubicBezier aRight; + + aBezierSegment.split(fBezierDistance, nullptr, &aRight); + aRetval.append(aRight.getStartPoint()); + aRetval.setNextControlPoint(aRetval.count() - 1, aRight.getControlPointA()); + bDone = true; + } + } + + if(!bDone) + { + const double fRelValue((fFrom - fPositionOfStart) / fEdgeLength); + aRetval.append(interpolate(aStart, aEnd, fRelValue)); + } + } + + bStartDone = true; + + // if same point, end is done, too. + if(rtl::math::approxEqual(fFrom, fTo)) + { + bEndDone = true; + } + } + } + + if(!bEndDone && fTools::moreOrEqual(fTo, fPositionOfStart) && fTools::less(fTo, fPositionOfStart + fEdgeLength)) + { + // calculate and add end point + if(fTools::equalZero(fEdgeLength)) + { + const sal_uInt32 nNextIndex((a + 1) % nPointCount); + aRetval.append(rCandidate.getB2DPoint(nNextIndex)); + + if(rCandidate.areControlPointsUsed()) + { + aRetval.setPrevControlPoint(aRetval.count() - 1, rCandidate.getPrevControlPoint(nNextIndex)); + } + } + else + { + const sal_uInt32 nNextIndex((a + 1) % nPointCount); + const B2DPoint aStart(rCandidate.getB2DPoint(a)); + const B2DPoint aEnd(rCandidate.getB2DPoint(nNextIndex)); + bool bDone(false); + + if(rCandidate.areControlPointsUsed()) + { + const B2DCubicBezier aBezierSegment( + aStart, rCandidate.getNextControlPoint(a), + rCandidate.getPrevControlPoint(nNextIndex), aEnd); + + if(aBezierSegment.isBezier()) + { + // use B2DCubicBezierHelper to bridge the non-linear gap between + // length and bezier distances + const B2DCubicBezierHelper aBezierSegmentHelper(aBezierSegment); + const double fBezierDistance(aBezierSegmentHelper.distanceToRelative(fTo - fPositionOfStart)); + B2DCubicBezier aLeft; + + aBezierSegment.split(fBezierDistance, &aLeft, nullptr); + aRetval.append(aLeft.getEndPoint()); + aRetval.setPrevControlPoint(aRetval.count() - 1, aLeft.getControlPointB()); + bDone = true; + } + } + + if(!bDone) + { + const double fRelValue((fTo - fPositionOfStart) / fEdgeLength); + aRetval.append(interpolate(aStart, aEnd, fRelValue)); + } + } + + bEndDone = true; + } + + if(!bEndDone) + { + if(bStartDone) + { + // add segments end point + const sal_uInt32 nNextIndex((a + 1) % nPointCount); + aRetval.append(rCandidate.getB2DPoint(nNextIndex)); + + if(rCandidate.areControlPointsUsed()) + { + aRetval.setPrevControlPoint(aRetval.count() - 1, rCandidate.getPrevControlPoint(nNextIndex)); + aRetval.setNextControlPoint(aRetval.count() - 1, rCandidate.getNextControlPoint(nNextIndex)); + } + } + + // increment fPositionOfStart + fPositionOfStart += fEdgeLength; + } + } + return aRetval; + } + } + else + { + return rCandidate; + } + } + + CutFlagValue findCut( + const B2DPoint& rEdge1Start, const B2DVector& rEdge1Delta, + const B2DPoint& rEdge2Start, const B2DVector& rEdge2Delta, + CutFlagValue aCutFlags, + double* pCut1, double* pCut2) + { + CutFlagValue aRetval(CutFlagValue::NONE); + double fCut1(0.0); + double fCut2(0.0); + bool bFinished(!static_cast<bool>(aCutFlags & CutFlagValue::ALL)); + + // test for same points? + if(!bFinished + && (aCutFlags & (CutFlagValue::START1|CutFlagValue::END1)) + && (aCutFlags & (CutFlagValue::START2|CutFlagValue::END2))) + { + // same startpoint? + if((aCutFlags & (CutFlagValue::START1|CutFlagValue::START2)) == (CutFlagValue::START1|CutFlagValue::START2)) + { + if(rEdge1Start.equal(rEdge2Start)) + { + bFinished = true; + aRetval = (CutFlagValue::START1|CutFlagValue::START2); + } + } + + // same endpoint? + if(!bFinished && (aCutFlags & (CutFlagValue::END1|CutFlagValue::END2)) == (CutFlagValue::END1|CutFlagValue::END2)) + { + const B2DPoint aEnd1(rEdge1Start + rEdge1Delta); + const B2DPoint aEnd2(rEdge2Start + rEdge2Delta); + + if(aEnd1.equal(aEnd2)) + { + bFinished = true; + aRetval = (CutFlagValue::END1|CutFlagValue::END2); + fCut1 = fCut2 = 1.0; + } + } + + // startpoint1 == endpoint2? + if(!bFinished && (aCutFlags & (CutFlagValue::START1|CutFlagValue::END2)) == (CutFlagValue::START1|CutFlagValue::END2)) + { + const B2DPoint aEnd2(rEdge2Start + rEdge2Delta); + + if(rEdge1Start.equal(aEnd2)) + { + bFinished = true; + aRetval = (CutFlagValue::START1|CutFlagValue::END2); + fCut1 = 0.0; + fCut2 = 1.0; + } + } + + // startpoint2 == endpoint1? + if(!bFinished&& (aCutFlags & (CutFlagValue::START2|CutFlagValue::END1)) == (CutFlagValue::START2|CutFlagValue::END1)) + { + const B2DPoint aEnd1(rEdge1Start + rEdge1Delta); + + if(rEdge2Start.equal(aEnd1)) + { + bFinished = true; + aRetval = (CutFlagValue::START2|CutFlagValue::END1); + fCut1 = 1.0; + fCut2 = 0.0; + } + } + } + + if(!bFinished && (aCutFlags & CutFlagValue::LINE)) + { + if(aCutFlags & CutFlagValue::START1) + { + // start1 on line 2 ? + if(isPointOnEdge(rEdge1Start, rEdge2Start, rEdge2Delta, &fCut2)) + { + bFinished = true; + aRetval = (CutFlagValue::LINE|CutFlagValue::START1); + } + } + + if(!bFinished && (aCutFlags & CutFlagValue::START2)) + { + // start2 on line 1 ? + if(isPointOnEdge(rEdge2Start, rEdge1Start, rEdge1Delta, &fCut1)) + { + bFinished = true; + aRetval = (CutFlagValue::LINE|CutFlagValue::START2); + } + } + + if(!bFinished && (aCutFlags & CutFlagValue::END1)) + { + // end1 on line 2 ? + const B2DPoint aEnd1(rEdge1Start + rEdge1Delta); + + if(isPointOnEdge(aEnd1, rEdge2Start, rEdge2Delta, &fCut2)) + { + bFinished = true; + aRetval = (CutFlagValue::LINE|CutFlagValue::END1); + } + } + + if(!bFinished && (aCutFlags & CutFlagValue::END2)) + { + // end2 on line 1 ? + const B2DPoint aEnd2(rEdge2Start + rEdge2Delta); + + if(isPointOnEdge(aEnd2, rEdge1Start, rEdge1Delta, &fCut1)) + { + bFinished = true; + aRetval = (CutFlagValue::LINE|CutFlagValue::END2); + } + } + + if(!bFinished) + { + // cut in line1, line2 ? + fCut1 = (rEdge1Delta.getX() * rEdge2Delta.getY()) - (rEdge1Delta.getY() * rEdge2Delta.getX()); + + if(!fTools::equalZero(fCut1)) + { + fCut1 = (rEdge2Delta.getY() * (rEdge2Start.getX() - rEdge1Start.getX()) + + rEdge2Delta.getX() * (rEdge1Start.getY() - rEdge2Start.getY())) / fCut1; + + const double fZero(0.0); + const double fOne(1.0); + + // inside parameter range edge1 AND fCut2 is calculable + if(fTools::more(fCut1, fZero) && fTools::less(fCut1, fOne) + && (!fTools::equalZero(rEdge2Delta.getX()) || !fTools::equalZero(rEdge2Delta.getY()))) + { + // take the more precise calculation of the two possible + if(fabs(rEdge2Delta.getX()) > fabs(rEdge2Delta.getY())) + { + fCut2 = (rEdge1Start.getX() + fCut1 + * rEdge1Delta.getX() - rEdge2Start.getX()) / rEdge2Delta.getX(); + } + else + { + fCut2 = (rEdge1Start.getY() + fCut1 + * rEdge1Delta.getY() - rEdge2Start.getY()) / rEdge2Delta.getY(); + } + + // inside parameter range edge2, too + if(fTools::more(fCut2, fZero) && fTools::less(fCut2, fOne)) + { + aRetval = CutFlagValue::LINE; + } + } + } + } + } + + // copy values if wanted + if(pCut1) + { + *pCut1 = fCut1; + } + + if(pCut2) + { + *pCut2 = fCut2; + } + + return aRetval; + } + + bool isPointOnEdge( + const B2DPoint& rPoint, + const B2DPoint& rEdgeStart, + const B2DVector& rEdgeDelta, + double* pCut) + { + bool bDeltaXIsZero(fTools::equalZero(rEdgeDelta.getX())); + bool bDeltaYIsZero(fTools::equalZero(rEdgeDelta.getY())); + const double fZero(0.0); + const double fOne(1.0); + + if(bDeltaXIsZero && bDeltaYIsZero) + { + // no line, just a point + return false; + } + else if(bDeltaXIsZero) + { + // vertical line + if(fTools::equal(rPoint.getX(), rEdgeStart.getX())) + { + double fValue = (rPoint.getY() - rEdgeStart.getY()) / rEdgeDelta.getY(); + + if(fTools::more(fValue, fZero) && fTools::less(fValue, fOne)) + { + if(pCut) + { + *pCut = fValue; + } + + return true; + } + } + } + else if(bDeltaYIsZero) + { + // horizontal line + if(fTools::equal(rPoint.getY(), rEdgeStart.getY())) + { + double fValue = (rPoint.getX() - rEdgeStart.getX()) / rEdgeDelta.getX(); + + if(fTools::more(fValue, fZero) && fTools::less(fValue, fOne)) + { + if(pCut) + { + *pCut = fValue; + } + + return true; + } + } + } + else + { + // any angle line + double fTOne = (rPoint.getX() - rEdgeStart.getX()) / rEdgeDelta.getX(); + double fTTwo = (rPoint.getY() - rEdgeStart.getY()) / rEdgeDelta.getY(); + + if(fTools::equal(fTOne, fTTwo)) + { + // same parameter representation, point is on line. Take + // middle value for better results + double fValue = (fTOne + fTTwo) / 2.0; + + if(fTools::more(fValue, fZero) && fTools::less(fValue, fOne)) + { + // point is inside line bounds, too + if(pCut) + { + *pCut = fValue; + } + + return true; + } + } + } + + return false; + } + + void applyLineDashing( + const B2DPolygon& rCandidate, + const std::vector<double>& rDotDashArray, + B2DPolyPolygon* pLineTarget, + B2DPolyPolygon* pGapTarget, + double fDotDashLength) + { + // clear targets in any case + if(pLineTarget) + { + pLineTarget->clear(); + } + + if(pGapTarget) + { + pGapTarget->clear(); + } + + // provide callbacks as lambdas + auto aLineCallback( + nullptr == pLineTarget + ? std::function<void(const basegfx::B2DPolygon&)>() + : [&pLineTarget](const basegfx::B2DPolygon& rSnippet){ pLineTarget->append(rSnippet); }); + auto aGapCallback( + nullptr == pGapTarget + ? std::function<void(const basegfx::B2DPolygon&)>() + : [&pGapTarget](const basegfx::B2DPolygon& rSnippet){ pGapTarget->append(rSnippet); }); + + // call version that uses callbacks + applyLineDashing( + rCandidate, + rDotDashArray, + aLineCallback, + aGapCallback, + fDotDashLength); + } + + static void implHandleSnippet( + const B2DPolygon& rSnippet, + const std::function<void(const basegfx::B2DPolygon& rSnippet)>& rTargetCallback, + B2DPolygon& rFirst, + B2DPolygon& rLast) + { + if(rSnippet.isClosed()) + { + if(!rFirst.count()) + { + rFirst = rSnippet; + } + else + { + if(rLast.count()) + { + rTargetCallback(rLast); + } + + rLast = rSnippet; + } + } + else + { + rTargetCallback(rSnippet); + } + } + + static void implHandleFirstLast( + const std::function<void(const basegfx::B2DPolygon& rSnippet)>& rTargetCallback, + B2DPolygon& rFirst, + B2DPolygon& rLast) + { + if(rFirst.count() && rLast.count() + && rFirst.getB2DPoint(0).equal(rLast.getB2DPoint(rLast.count() - 1))) + { + // start of first and end of last are the same -> merge them + rLast.append(rFirst); + rLast.removeDoublePoints(); + rFirst.clear(); + } + + if(rLast.count()) + { + rTargetCallback(rLast); + } + + if(rFirst.count()) + { + rTargetCallback(rFirst); + } + } + + void applyLineDashing( + const B2DPolygon& rCandidate, + const std::vector<double>& rDotDashArray, + const std::function<void(const basegfx::B2DPolygon& rSnippet)>& rLineTargetCallback, + const std::function<void(const basegfx::B2DPolygon& rSnippet)>& rGapTargetCallback, + double fDotDashLength) + { + const sal_uInt32 nPointCount(rCandidate.count()); + const sal_uInt32 nDotDashCount(rDotDashArray.size()); + + if(fTools::lessOrEqual(fDotDashLength, 0.0)) + { + fDotDashLength = std::accumulate(rDotDashArray.begin(), rDotDashArray.end(), 0.0); + } + + if(fTools::lessOrEqual(fDotDashLength, 0.0) || (!rLineTargetCallback && !rGapTargetCallback) || !nPointCount) + { + // parameters make no sense, just add source to targets + if (rLineTargetCallback) + rLineTargetCallback(rCandidate); + + if (rGapTargetCallback) + rGapTargetCallback(rCandidate); + + return; + } + + // precalculate maximal acceptable length of candidate polygon assuming + // we want to create a maximum of fNumberOfAllowedSnippets. For + // fNumberOfAllowedSnippets use ca. 65536, double due to line & gap. + static const double fNumberOfAllowedSnippets(100.0 * 2.0); + const double fAllowedLength((fNumberOfAllowedSnippets * fDotDashLength) / double(rDotDashArray.size())); + const double fCandidateLength(basegfx::utils::getLength(rCandidate, /*bApproximateBezierLength*/true)); + std::vector<double> aDotDashArray(rDotDashArray); + + if(fCandidateLength > fAllowedLength) + { + // we would produce more than fNumberOfAllowedSnippets, so + // adapt aDotDashArray to exactly produce assumed number. Also + // assert this to let the caller know about it. + // If this asserts: Please think about checking your DotDashArray + // before calling this function or evtl. use the callback version + // to *not* produce that much of data. Even then, you may still + // think about producing too much runtime (!) + assert(true && "applyLineDashing: potentially too expensive to do the requested dismantle - please consider stretched LineDash pattern (!)"); + + // calculate correcting factor, apply to aDotDashArray and fDotDashLength + // to enlarge these as needed + const double fFactor(fCandidateLength / fAllowedLength); + std::for_each(aDotDashArray.begin(), aDotDashArray.end(), [&fFactor](double &f){ f *= fFactor; }); + } + + // prepare current edge's start + B2DCubicBezier aCurrentEdge; + const bool bIsClosed(rCandidate.isClosed()); + const sal_uInt32 nEdgeCount(bIsClosed ? nPointCount : nPointCount - 1); + aCurrentEdge.setStartPoint(rCandidate.getB2DPoint(0)); + + // prepare DotDashArray iteration and the line/gap switching bool + sal_uInt32 nDotDashIndex(0); + bool bIsLine(true); + double fDotDashMovingLength(aDotDashArray[0]); + B2DPolygon aSnippet; + + // remember 1st and last snippets to try to merge after execution + // is complete and hand to callback + B2DPolygon aFirstLine, aLastLine; + B2DPolygon aFirstGap, aLastGap; + + // iterate over all edges + for(sal_uInt32 a(0); a < nEdgeCount; a++) + { + // update current edge (fill in C1, C2 and end point) + double fLastDotDashMovingLength(0.0); + const sal_uInt32 nNextIndex((a + 1) % nPointCount); + aCurrentEdge.setControlPointA(rCandidate.getNextControlPoint(a)); + aCurrentEdge.setControlPointB(rCandidate.getPrevControlPoint(nNextIndex)); + aCurrentEdge.setEndPoint(rCandidate.getB2DPoint(nNextIndex)); + + // check if we have a trivial bezier segment -> possible fallback to edge + aCurrentEdge.testAndSolveTrivialBezier(); + + if(aCurrentEdge.isBezier()) + { + // bezier segment + const B2DCubicBezierHelper aCubicBezierHelper(aCurrentEdge); + const double fEdgeLength(aCubicBezierHelper.getLength()); + + if(!fTools::equalZero(fEdgeLength)) + { + while(fTools::less(fDotDashMovingLength, fEdgeLength)) + { + // new split is inside edge, create and append snippet [fLastDotDashMovingLength, fDotDashMovingLength] + const bool bHandleLine(bIsLine && rLineTargetCallback); + const bool bHandleGap(!bIsLine && rGapTargetCallback); + + if(bHandleLine || bHandleGap) + { + const double fBezierSplitStart(aCubicBezierHelper.distanceToRelative(fLastDotDashMovingLength)); + const double fBezierSplitEnd(aCubicBezierHelper.distanceToRelative(fDotDashMovingLength)); + B2DCubicBezier aBezierSnippet(aCurrentEdge.snippet(fBezierSplitStart, fBezierSplitEnd)); + + if(!aSnippet.count()) + { + aSnippet.append(aBezierSnippet.getStartPoint()); + } + + aSnippet.appendBezierSegment(aBezierSnippet.getControlPointA(), aBezierSnippet.getControlPointB(), aBezierSnippet.getEndPoint()); + + if(bHandleLine) + { + implHandleSnippet(aSnippet, rLineTargetCallback, aFirstLine, aLastLine); + } + + if(bHandleGap) + { + implHandleSnippet(aSnippet, rGapTargetCallback, aFirstGap, aLastGap); + } + + aSnippet.clear(); + } + + // prepare next DotDashArray step and flip line/gap flag + fLastDotDashMovingLength = fDotDashMovingLength; + fDotDashMovingLength += aDotDashArray[(++nDotDashIndex) % nDotDashCount]; + bIsLine = !bIsLine; + } + + // append closing snippet [fLastDotDashMovingLength, fEdgeLength] + const bool bHandleLine(bIsLine && rLineTargetCallback); + const bool bHandleGap(!bIsLine && rGapTargetCallback); + + if(bHandleLine || bHandleGap) + { + B2DCubicBezier aRight; + const double fBezierSplit(aCubicBezierHelper.distanceToRelative(fLastDotDashMovingLength)); + + aCurrentEdge.split(fBezierSplit, nullptr, &aRight); + + if(!aSnippet.count()) + { + aSnippet.append(aRight.getStartPoint()); + } + + aSnippet.appendBezierSegment(aRight.getControlPointA(), aRight.getControlPointB(), aRight.getEndPoint()); + } + + // prepare move to next edge + fDotDashMovingLength -= fEdgeLength; + } + } + else + { + // simple edge + const double fEdgeLength(aCurrentEdge.getEdgeLength()); + + if(!fTools::equalZero(fEdgeLength)) + { + while(fTools::less(fDotDashMovingLength, fEdgeLength)) + { + // new split is inside edge, create and append snippet [fLastDotDashMovingLength, fDotDashMovingLength] + const bool bHandleLine(bIsLine && rLineTargetCallback); + const bool bHandleGap(!bIsLine && rGapTargetCallback); + + if(bHandleLine || bHandleGap) + { + if(!aSnippet.count()) + { + aSnippet.append(interpolate(aCurrentEdge.getStartPoint(), aCurrentEdge.getEndPoint(), fLastDotDashMovingLength / fEdgeLength)); + } + + aSnippet.append(interpolate(aCurrentEdge.getStartPoint(), aCurrentEdge.getEndPoint(), fDotDashMovingLength / fEdgeLength)); + + if(bHandleLine) + { + implHandleSnippet(aSnippet, rLineTargetCallback, aFirstLine, aLastLine); + } + + if(bHandleGap) + { + implHandleSnippet(aSnippet, rGapTargetCallback, aFirstGap, aLastGap); + } + + aSnippet.clear(); + } + + // prepare next DotDashArray step and flip line/gap flag + fLastDotDashMovingLength = fDotDashMovingLength; + fDotDashMovingLength += aDotDashArray[(++nDotDashIndex) % nDotDashCount]; + bIsLine = !bIsLine; + } + + // append snippet [fLastDotDashMovingLength, fEdgeLength] + const bool bHandleLine(bIsLine && rLineTargetCallback); + const bool bHandleGap(!bIsLine && rGapTargetCallback); + + if(bHandleLine || bHandleGap) + { + if(!aSnippet.count()) + { + aSnippet.append(interpolate(aCurrentEdge.getStartPoint(), aCurrentEdge.getEndPoint(), fLastDotDashMovingLength / fEdgeLength)); + } + + aSnippet.append(aCurrentEdge.getEndPoint()); + } + + // prepare move to next edge + fDotDashMovingLength -= fEdgeLength; + } + } + + // prepare next edge step (end point gets new start point) + aCurrentEdge.setStartPoint(aCurrentEdge.getEndPoint()); + } + + // append last intermediate results (if exists) + if(aSnippet.count()) + { + const bool bHandleLine(bIsLine && rLineTargetCallback); + const bool bHandleGap(!bIsLine && rGapTargetCallback); + + if(bHandleLine) + { + implHandleSnippet(aSnippet, rLineTargetCallback, aFirstLine, aLastLine); + } + + if(bHandleGap) + { + implHandleSnippet(aSnippet, rGapTargetCallback, aFirstGap, aLastGap); + } + } + + if(bIsClosed && rLineTargetCallback) + { + implHandleFirstLast(rLineTargetCallback, aFirstLine, aLastLine); + } + + if(bIsClosed && rGapTargetCallback) + { + implHandleFirstLast(rGapTargetCallback, aFirstGap, aLastGap); + } + } + + // test if point is inside epsilon-range around an edge defined + // by the two given points. Can be used for HitTesting. The epsilon-range + // is defined to be the rectangle centered to the given edge, using height + // 2 x fDistance, and the circle around both points with radius fDistance. + bool isInEpsilonRange(const B2DPoint& rEdgeStart, const B2DPoint& rEdgeEnd, const B2DPoint& rTestPosition, double fDistance) + { + // build edge vector + const B2DVector aEdge(rEdgeEnd - rEdgeStart); + bool bDoDistanceTestStart(false); + bool bDoDistanceTestEnd(false); + + if(aEdge.equalZero()) + { + // no edge, just a point. Do one of the distance tests. + bDoDistanceTestStart = true; + } + else + { + // edge has a length. Create perpendicular vector. + const B2DVector aPerpend(getPerpendicular(aEdge)); + double fCut( + (aPerpend.getY() * (rTestPosition.getX() - rEdgeStart.getX()) + + aPerpend.getX() * (rEdgeStart.getY() - rTestPosition.getY())) / + (aEdge.getX() * aEdge.getX() + aEdge.getY() * aEdge.getY())); + const double fZero(0.0); + const double fOne(1.0); + + if(fTools::less(fCut, fZero)) + { + // left of rEdgeStart + bDoDistanceTestStart = true; + } + else if(fTools::more(fCut, fOne)) + { + // right of rEdgeEnd + bDoDistanceTestEnd = true; + } + else + { + // inside line [0.0 .. 1.0] + const B2DPoint aCutPoint(interpolate(rEdgeStart, rEdgeEnd, fCut)); + const B2DVector aDelta(rTestPosition - aCutPoint); + const double fDistanceSquare(aDelta.scalar(aDelta)); + + return fDistanceSquare <= fDistance * fDistance; + } + } + + if(bDoDistanceTestStart) + { + const B2DVector aDelta(rTestPosition - rEdgeStart); + const double fDistanceSquare(aDelta.scalar(aDelta)); + + if(fDistanceSquare <= fDistance * fDistance) + { + return true; + } + } + else if(bDoDistanceTestEnd) + { + const B2DVector aDelta(rTestPosition - rEdgeEnd); + const double fDistanceSquare(aDelta.scalar(aDelta)); + + if(fDistanceSquare <= fDistance * fDistance) + { + return true; + } + } + + return false; + } + + // test if point is inside epsilon-range around the given Polygon. Can be used + // for HitTesting. The epsilon-range is defined to be the tube around the polygon + // with distance fDistance and rounded edges (start and end point). + bool isInEpsilonRange(const B2DPolygon& rCandidate, const B2DPoint& rTestPosition, double fDistance) + { + // force to non-bezier polygon + const B2DPolygon& aCandidate(rCandidate.getDefaultAdaptiveSubdivision()); + const sal_uInt32 nPointCount(aCandidate.count()); + + if(!nPointCount) + return false; + + const sal_uInt32 nEdgeCount(aCandidate.isClosed() ? nPointCount : nPointCount - 1); + B2DPoint aCurrent(aCandidate.getB2DPoint(0)); + + if(nEdgeCount) + { + // edges + for(sal_uInt32 a(0); a < nEdgeCount; a++) + { + const sal_uInt32 nNextIndex((a + 1) % nPointCount); + const B2DPoint aNext(aCandidate.getB2DPoint(nNextIndex)); + + if(isInEpsilonRange(aCurrent, aNext, rTestPosition, fDistance)) + { + return true; + } + + // prepare next step + aCurrent = aNext; + } + } + else + { + // no edges, but points -> not closed. Check single point. Just + // use isInEpsilonRange with twice the same point, it handles those well + if(isInEpsilonRange(aCurrent, aCurrent, rTestPosition, fDistance)) + { + return true; + } + } + + return false; + } + + // Calculates distance of curve point to its control point for a Bézier curve, that + // approximates a unit circle arc. fAngle is the center angle of the circle arc. The + // constrain 0<=fAngle<=pi/2 must not be violated to give a useful accuracy. For details + // and alternatives read document "ApproxCircleInfo.odt", attachment of bug tdf#121425. + static double impDistanceBezierPointToControl(double fAngle) + { + SAL_WARN_IF(fAngle < 0 || fAngle > M_PI_2,"basegfx","angle not suitable for approximate circle"); + if (0 <= fAngle && fAngle <= M_PI_2) + { + return 4.0/3.0 * ( tan(fAngle/4.0)); + } + else + return 0; + } + + B2DPolygon createPolygonFromRect( const B2DRectangle& rRect, double fRadiusX, double fRadiusY ) + { + const double fZero(0.0); + const double fOne(1.0); + + fRadiusX = std::clamp(fRadiusX, 0.0, 1.0); + fRadiusY = std::clamp(fRadiusY, 0.0, 1.0); + + if(rtl::math::approxEqual(fZero, fRadiusX) || rtl::math::approxEqual(fZero, fRadiusY)) + { + // at least in one direction no radius, use rectangle. + // Do not use createPolygonFromRect() here since original + // creator (historical reasons) still creates a start point at the + // bottom center, so do the same here to get the same line patterns. + // Due to this the order of points is different, too. + const B2DPoint aBottomCenter(rRect.getCenter().getX(), rRect.getMaxY()); + B2DPolygon aPolygon { + aBottomCenter, + { rRect.getMinX(), rRect.getMaxY() }, + { rRect.getMinX(), rRect.getMinY() }, + { rRect.getMaxX(), rRect.getMinY() }, + { rRect.getMaxX(), rRect.getMaxY() } + }; + + // close + aPolygon.setClosed( true ); + + return aPolygon; + } + else if(rtl::math::approxEqual(fOne, fRadiusX) && rtl::math::approxEqual(fOne, fRadiusY)) + { + // in both directions full radius, use ellipse + const B2DPoint aCenter(rRect.getCenter()); + const double fRectRadiusX(rRect.getWidth() / 2.0); + const double fRectRadiusY(rRect.getHeight() / 2.0); + + return createPolygonFromEllipse( aCenter, fRectRadiusX, fRectRadiusY ); + } + else + { + B2DPolygon aRetval; + const double fBowX((rRect.getWidth() / 2.0) * fRadiusX); + const double fBowY((rRect.getHeight() / 2.0) * fRadiusY); + const double fKappa(impDistanceBezierPointToControl(M_PI_2)); + + // create start point at bottom center + if(!rtl::math::approxEqual(fOne, fRadiusX)) + { + const B2DPoint aBottomCenter(rRect.getCenter().getX(), rRect.getMaxY()); + aRetval.append(aBottomCenter); + } + + // create first bow + { + const B2DPoint aBottomRight(rRect.getMaxX(), rRect.getMaxY()); + const B2DPoint aStart(aBottomRight + B2DPoint(-fBowX, 0.0)); + const B2DPoint aStop(aBottomRight + B2DPoint(0.0, -fBowY)); + aRetval.append(aStart); + aRetval.appendBezierSegment(interpolate(aStart, aBottomRight, fKappa), interpolate(aStop, aBottomRight, fKappa), aStop); + } + + // create second bow + { + const B2DPoint aTopRight(rRect.getMaxX(), rRect.getMinY()); + const B2DPoint aStart(aTopRight + B2DPoint(0.0, fBowY)); + const B2DPoint aStop(aTopRight + B2DPoint(-fBowX, 0.0)); + aRetval.append(aStart); + aRetval.appendBezierSegment(interpolate(aStart, aTopRight, fKappa), interpolate(aStop, aTopRight, fKappa), aStop); + } + + // create third bow + { + const B2DPoint aTopLeft(rRect.getMinX(), rRect.getMinY()); + const B2DPoint aStart(aTopLeft + B2DPoint(fBowX, 0.0)); + const B2DPoint aStop(aTopLeft + B2DPoint(0.0, fBowY)); + aRetval.append(aStart); + aRetval.appendBezierSegment(interpolate(aStart, aTopLeft, fKappa), interpolate(aStop, aTopLeft, fKappa), aStop); + } + + // create forth bow + { + const B2DPoint aBottomLeft(rRect.getMinX(), rRect.getMaxY()); + const B2DPoint aStart(aBottomLeft + B2DPoint(0.0, -fBowY)); + const B2DPoint aStop(aBottomLeft + B2DPoint(fBowX, 0.0)); + aRetval.append(aStart); + aRetval.appendBezierSegment(interpolate(aStart, aBottomLeft, fKappa), interpolate(aStop, aBottomLeft, fKappa), aStop); + } + + // close + aRetval.setClosed( true ); + + // remove double created points if there are extreme radii involved + if(rtl::math::approxEqual(fOne, fRadiusX) || rtl::math::approxEqual(fOne, fRadiusY)) + { + aRetval.removeDoublePoints(); + } + + return aRetval; + } + } + + B2DPolygon createPolygonFromRect( const B2DRectangle& rRect ) + { + B2DPolygon aPolygon { + { rRect.getMinX(), rRect.getMinY() }, + { rRect.getMaxX(), rRect.getMinY() }, + { rRect.getMaxX(), rRect.getMaxY() }, + { rRect.getMinX(), rRect.getMaxY() } + }; + + // close + aPolygon.setClosed( true ); + + return aPolygon; + } + + B2DPolygon const & createUnitPolygon() + { + static auto const singleton = [] { + B2DPolygon aPolygon { + { 0.0, 0.0 }, + { 1.0, 0.0 }, + { 1.0, 1.0 }, + { 0.0, 1.0 } + }; + + // close + aPolygon.setClosed( true ); + + return aPolygon; + }(); + return singleton; + } + + B2DPolygon createPolygonFromCircle( const B2DPoint& rCenter, double fRadius ) + { + return createPolygonFromEllipse( rCenter, fRadius, fRadius ); + } + + static B2DPolygon impCreateUnitCircle(sal_uInt32 nStartQuadrant) + { + B2DPolygon aUnitCircle; + const double fSegmentKappa = impDistanceBezierPointToControl(M_PI_2 / STEPSPERQUARTER); + const B2DHomMatrix aRotateMatrix(createRotateB2DHomMatrix(M_PI_2 / STEPSPERQUARTER)); + + B2DPoint aPoint(1.0, 0.0); + B2DPoint aForward(1.0, fSegmentKappa); + B2DPoint aBackward(1.0, -fSegmentKappa); + + if(nStartQuadrant != 0) + { + const B2DHomMatrix aQuadrantMatrix(createRotateB2DHomMatrix(M_PI_2 * (nStartQuadrant % 4))); + aPoint *= aQuadrantMatrix; + aBackward *= aQuadrantMatrix; + aForward *= aQuadrantMatrix; + } + + aUnitCircle.append(aPoint); + + for(sal_uInt32 a(0); a < STEPSPERQUARTER * 4; a++) + { + aPoint *= aRotateMatrix; + aBackward *= aRotateMatrix; + aUnitCircle.appendBezierSegment(aForward, aBackward, aPoint); + aForward *= aRotateMatrix; + } + + aUnitCircle.setClosed(true); + aUnitCircle.removeDoublePoints(); + + return aUnitCircle; + } + + B2DPolygon const & createHalfUnitCircle() + { + static auto const singleton = [] { + B2DPolygon aUnitHalfCircle; + const double fSegmentKappa(impDistanceBezierPointToControl(M_PI_2 / STEPSPERQUARTER)); + const B2DHomMatrix aRotateMatrix(createRotateB2DHomMatrix(M_PI_2 / STEPSPERQUARTER)); + B2DPoint aPoint(1.0, 0.0); + B2DPoint aForward(1.0, fSegmentKappa); + B2DPoint aBackward(1.0, -fSegmentKappa); + + aUnitHalfCircle.append(aPoint); + + for(sal_uInt32 a(0); a < STEPSPERQUARTER * 2; a++) + { + aPoint *= aRotateMatrix; + aBackward *= aRotateMatrix; + aUnitHalfCircle.appendBezierSegment(aForward, aBackward, aPoint); + aForward *= aRotateMatrix; + } + return aUnitHalfCircle; + }(); + return singleton; + } + + B2DPolygon const & createPolygonFromUnitCircle(sal_uInt32 nStartQuadrant) + { + switch(nStartQuadrant % 4) + { + case 1 : + { + static auto const singleton = impCreateUnitCircle(1); + return singleton; + } + + case 2 : + { + static auto const singleton = impCreateUnitCircle(2); + return singleton; + } + + case 3 : + { + static auto const singleton = impCreateUnitCircle(3); + return singleton; + } + + default : // case 0 : + { + static auto const singleton = impCreateUnitCircle(0); + return singleton; + } + } + } + + B2DPolygon createPolygonFromEllipse( const B2DPoint& rCenter, double fRadiusX, double fRadiusY, sal_uInt32 nStartQuadrant) + { + B2DPolygon aRetval(createPolygonFromUnitCircle(nStartQuadrant)); + const B2DHomMatrix aMatrix(createScaleTranslateB2DHomMatrix(fRadiusX, fRadiusY, rCenter.getX(), rCenter.getY())); + + aRetval.transform(aMatrix); + + return aRetval; + } + + B2DPolygon createPolygonFromUnitEllipseSegment( double fStart, double fEnd ) + { + B2DPolygon aRetval; + + // truncate fStart, fEnd to a range of [0.0 .. 2PI[ where 2PI + // falls back to 0.0 to ensure a unique definition + if(fTools::less(fStart, 0.0)) + { + fStart = 0.0; + } + + if(fTools::moreOrEqual(fStart, 2 * M_PI)) + { + fStart = 0.0; + } + + if(fTools::less(fEnd, 0.0)) + { + fEnd = 0.0; + } + + if(fTools::moreOrEqual(fEnd, 2 * M_PI)) + { + fEnd = 0.0; + } + + if(fTools::equal(fStart, fEnd)) + { + // same start and end angle, add single point + aRetval.append(B2DPoint(cos(fStart), sin(fStart))); + } + else + { + const sal_uInt32 nSegments(STEPSPERQUARTER * 4); + const double fAnglePerSegment(M_PI_2 / STEPSPERQUARTER); + const sal_uInt32 nStartSegment(sal_uInt32(fStart / fAnglePerSegment) % nSegments); + const sal_uInt32 nEndSegment(sal_uInt32(fEnd / fAnglePerSegment) % nSegments); + const double fSegmentKappa(impDistanceBezierPointToControl(fAnglePerSegment)); + + B2DPoint aSegStart(cos(fStart), sin(fStart)); + aRetval.append(aSegStart); + + if(nStartSegment == nEndSegment && fTools::more(fEnd, fStart)) + { + // start and end in one sector and in the right order, create in one segment + const B2DPoint aSegEnd(cos(fEnd), sin(fEnd)); + const double fFactor(impDistanceBezierPointToControl(fEnd - fStart)); + + aRetval.appendBezierSegment( + aSegStart + (B2DPoint(-aSegStart.getY(), aSegStart.getX()) * fFactor), + aSegEnd - (B2DPoint(-aSegEnd.getY(), aSegEnd.getX()) * fFactor), + aSegEnd); + } + else + { + double fSegEndRad((nStartSegment + 1) * fAnglePerSegment); + double fFactor(impDistanceBezierPointToControl(fSegEndRad - fStart)); + B2DPoint aSegEnd(cos(fSegEndRad), sin(fSegEndRad)); + + aRetval.appendBezierSegment( + aSegStart + (B2DPoint(-aSegStart.getY(), aSegStart.getX()) * fFactor), + aSegEnd - (B2DPoint(-aSegEnd.getY(), aSegEnd.getX()) * fFactor), + aSegEnd); + + sal_uInt32 nSegment((nStartSegment + 1) % nSegments); + aSegStart = aSegEnd; + + while(nSegment != nEndSegment) + { + // No end in this sector, add full sector. + fSegEndRad = (nSegment + 1) * fAnglePerSegment; + aSegEnd = B2DPoint(cos(fSegEndRad), sin(fSegEndRad)); + + aRetval.appendBezierSegment( + aSegStart + (B2DPoint(-aSegStart.getY(), aSegStart.getX()) * fSegmentKappa), + aSegEnd - (B2DPoint(-aSegEnd.getY(), aSegEnd.getX()) * fSegmentKappa), + aSegEnd); + + nSegment = (nSegment + 1) % nSegments; + aSegStart = aSegEnd; + } + + // End in this sector + const double fSegStartRad(nSegment * fAnglePerSegment); + fFactor= impDistanceBezierPointToControl(fEnd - fSegStartRad); + aSegEnd = B2DPoint(cos(fEnd), sin(fEnd)); + + aRetval.appendBezierSegment( + aSegStart + (B2DPoint(-aSegStart.getY(), aSegStart.getX()) * fFactor), + aSegEnd - (B2DPoint(-aSegEnd.getY(), aSegEnd.getX()) * fFactor), + aSegEnd); + } + } + + // remove double points between segments created by segmented creation + aRetval.removeDoublePoints(); + + return aRetval; + } + + B2DPolygon createPolygonFromEllipseSegment( const B2DPoint& rCenter, double fRadiusX, double fRadiusY, double fStart, double fEnd ) + { + B2DPolygon aRetval(createPolygonFromUnitEllipseSegment(fStart, fEnd)); + const B2DHomMatrix aMatrix(createScaleTranslateB2DHomMatrix(fRadiusX, fRadiusY, rCenter.getX(), rCenter.getY())); + + aRetval.transform(aMatrix); + + return aRetval; + } + + bool hasNeutralPoints(const B2DPolygon& rCandidate) + { + OSL_ENSURE(!rCandidate.areControlPointsUsed(), "hasNeutralPoints: ATM works not for curves (!)"); + const sal_uInt32 nPointCount(rCandidate.count()); + + if(nPointCount <= 2) + return false; + + B2DPoint aPrevPoint(rCandidate.getB2DPoint(nPointCount - 1)); + B2DPoint aCurrPoint(rCandidate.getB2DPoint(0)); + + for(sal_uInt32 a(0); a < nPointCount; a++) + { + const B2DPoint aNextPoint(rCandidate.getB2DPoint((a + 1) % nPointCount)); + const B2DVector aPrevVec(aPrevPoint - aCurrPoint); + const B2DVector aNextVec(aNextPoint - aCurrPoint); + const B2VectorOrientation aOrientation(getOrientation(aNextVec, aPrevVec)); + + if(aOrientation == B2VectorOrientation::Neutral) + { + // current has neutral orientation + return true; + } + else + { + // prepare next + aPrevPoint = aCurrPoint; + aCurrPoint = aNextPoint; + } + } + + return false; + } + + B2DPolygon removeNeutralPoints(const B2DPolygon& rCandidate) + { + if(hasNeutralPoints(rCandidate)) + { + const sal_uInt32 nPointCount(rCandidate.count()); + B2DPolygon aRetval; + B2DPoint aPrevPoint(rCandidate.getB2DPoint(nPointCount - 1)); + B2DPoint aCurrPoint(rCandidate.getB2DPoint(0)); + + for(sal_uInt32 a(0); a < nPointCount; a++) + { + const B2DPoint aNextPoint(rCandidate.getB2DPoint((a + 1) % nPointCount)); + const B2DVector aPrevVec(aPrevPoint - aCurrPoint); + const B2DVector aNextVec(aNextPoint - aCurrPoint); + const B2VectorOrientation aOrientation(getOrientation(aNextVec, aPrevVec)); + + if(aOrientation == B2VectorOrientation::Neutral) + { + // current has neutral orientation, leave it out and prepare next + aCurrPoint = aNextPoint; + } + else + { + // add current point + aRetval.append(aCurrPoint); + + // prepare next + aPrevPoint = aCurrPoint; + aCurrPoint = aNextPoint; + } + } + + while(aRetval.count() && getOrientationForIndex(aRetval, 0) == B2VectorOrientation::Neutral) + { + aRetval.remove(0); + } + + // copy closed state + aRetval.setClosed(rCandidate.isClosed()); + + return aRetval; + } + else + { + return rCandidate; + } + } + + bool isConvex(const B2DPolygon& rCandidate) + { + OSL_ENSURE(!rCandidate.areControlPointsUsed(), "isConvex: ATM works not for curves (!)"); + const sal_uInt32 nPointCount(rCandidate.count()); + + if(nPointCount <= 2) + return true; + + const B2DPoint aPrevPoint(rCandidate.getB2DPoint(nPointCount - 1)); + B2DPoint aCurrPoint(rCandidate.getB2DPoint(0)); + B2DVector aCurrVec(aPrevPoint - aCurrPoint); + B2VectorOrientation aOrientation(B2VectorOrientation::Neutral); + + for(sal_uInt32 a(0); a < nPointCount; a++) + { + const B2DPoint aNextPoint(rCandidate.getB2DPoint((a + 1) % nPointCount)); + const B2DVector aNextVec(aNextPoint - aCurrPoint); + const B2VectorOrientation aCurrentOrientation(getOrientation(aNextVec, aCurrVec)); + + if(aOrientation == B2VectorOrientation::Neutral) + { + // set start value, maybe neutral again + aOrientation = aCurrentOrientation; + } + else + { + if(aCurrentOrientation != B2VectorOrientation::Neutral && aCurrentOrientation != aOrientation) + { + // different orientations found, that's it + return false; + } + } + + // prepare next + aCurrPoint = aNextPoint; + aCurrVec = -aNextVec; + } + + return true; + } + + B2VectorOrientation getOrientationForIndex(const B2DPolygon& rCandidate, sal_uInt32 nIndex) + { + OSL_ENSURE(nIndex < rCandidate.count(), "getOrientationForIndex: index out of range (!)"); + const B2DPoint aPrev(rCandidate.getB2DPoint(getIndexOfPredecessor(nIndex, rCandidate))); + const B2DPoint aCurr(rCandidate.getB2DPoint(nIndex)); + const B2DPoint aNext(rCandidate.getB2DPoint(getIndexOfSuccessor(nIndex, rCandidate))); + const B2DVector aBack(aPrev - aCurr); + const B2DVector aForw(aNext - aCurr); + + return getOrientation(aForw, aBack); + } + + bool isPointOnLine(const B2DPoint& rStart, const B2DPoint& rEnd, const B2DPoint& rCandidate, bool bWithPoints) + { + if(rCandidate.equal(rStart) || rCandidate.equal(rEnd)) + { + // candidate is in epsilon around start or end -> inside + return bWithPoints; + } + else if(rStart.equal(rEnd)) + { + // start and end are equal, but candidate is outside their epsilon -> outside + return false; + } + else + { + const B2DVector aEdgeVector(rEnd - rStart); + const B2DVector aTestVector(rCandidate - rStart); + + if(areParallel(aEdgeVector, aTestVector)) + { + const double fZero(0.0); + const double fOne(1.0); + const double fParamTestOnCurr(fabs(aEdgeVector.getX()) > fabs(aEdgeVector.getY()) + ? aTestVector.getX() / aEdgeVector.getX() + : aTestVector.getY() / aEdgeVector.getY()); + + if(fTools::more(fParamTestOnCurr, fZero) && fTools::less(fParamTestOnCurr, fOne)) + { + return true; + } + } + + return false; + } + } + + bool isPointOnPolygon(const B2DPolygon& rCandidate, const B2DPoint& rPoint, bool bWithPoints) + { + const B2DPolygon aCandidate(rCandidate.areControlPointsUsed() ? rCandidate.getDefaultAdaptiveSubdivision() : rCandidate); + const sal_uInt32 nPointCount(aCandidate.count()); + + if(nPointCount > 1) + { + const sal_uInt32 nLoopCount(aCandidate.isClosed() ? nPointCount : nPointCount - 1); + B2DPoint aCurrentPoint(aCandidate.getB2DPoint(0)); + + for(sal_uInt32 a(0); a < nLoopCount; a++) + { + const B2DPoint aNextPoint(aCandidate.getB2DPoint((a + 1) % nPointCount)); + + if(isPointOnLine(aCurrentPoint, aNextPoint, rPoint, bWithPoints)) + { + return true; + } + + aCurrentPoint = aNextPoint; + } + } + else if(nPointCount && bWithPoints) + { + return rPoint.equal(aCandidate.getB2DPoint(0)); + } + + return false; + } + + bool isPointInTriangle(const B2DPoint& rA, const B2DPoint& rB, const B2DPoint& rC, const B2DPoint& rCandidate, bool bWithBorder) + { + if(arePointsOnSameSideOfLine(rA, rB, rC, rCandidate, bWithBorder)) + { + if(arePointsOnSameSideOfLine(rB, rC, rA, rCandidate, bWithBorder)) + { + if(arePointsOnSameSideOfLine(rC, rA, rB, rCandidate, bWithBorder)) + { + return true; + } + } + } + + return false; + } + + bool arePointsOnSameSideOfLine(const B2DPoint& rStart, const B2DPoint& rEnd, const B2DPoint& rCandidateA, const B2DPoint& rCandidateB, bool bWithLine) + { + const B2DVector aLineVector(rEnd - rStart); + const B2DVector aVectorToA(rEnd - rCandidateA); + const double fCrossA(aLineVector.cross(aVectorToA)); + + // tdf#88352 increase numerical correctness and use rtl::math::approxEqual + // instead of fTools::equalZero which compares with a fixed small value + if(fCrossA == 0.0) + { + // one point on the line + return bWithLine; + } + + const B2DVector aVectorToB(rEnd - rCandidateB); + const double fCrossB(aLineVector.cross(aVectorToB)); + + // increase numerical correctness + if(fCrossB == 0.0) + { + // one point on the line + return bWithLine; + } + + // return true if they both have the same sign + return ((fCrossA > 0.0) == (fCrossB > 0.0)); + } + + void addTriangleFan( + const B2DPolygon& rCandidate, + triangulator::B2DTriangleVector& rTarget) + { + const sal_uInt32 nCount(rCandidate.count()); + + if(nCount <= 2) + return; + + const B2DPoint aStart(rCandidate.getB2DPoint(0)); + B2DPoint aLast(rCandidate.getB2DPoint(1)); + + for(sal_uInt32 a(2); a < nCount; a++) + { + const B2DPoint aCurrent(rCandidate.getB2DPoint(a)); + rTarget.emplace_back( + aStart, + aLast, + aCurrent); + + // prepare next + aLast = aCurrent; + } + } + + namespace + { + /// return 0 for input of 0, -1 for negative and 1 for positive input + int lcl_sgn( const double n ) + { + return n == 0.0 ? 0 : 1 - 2*int(std::signbit(n)); + } + } + + bool isRectangle( const B2DPolygon& rPoly ) + { + // polygon must be closed to resemble a rect, and contain + // at least four points. + if( !rPoly.isClosed() || + rPoly.count() < 4 || + rPoly.areControlPointsUsed() ) + { + return false; + } + + // number of 90 degree turns the polygon has taken + int nNumTurns(0); + + int nVerticalEdgeType=0; + int nHorizontalEdgeType=0; + bool bNullVertex(true); + bool bCWPolygon(false); // when true, polygon is CW + // oriented, when false, CCW + bool bOrientationSet(false); // when false, polygon + // orientation has not yet + // been determined. + + // scan all _edges_ (which involves coming back to point 0 + // for the last edge - thus the modulo operation below) + const sal_Int32 nCount( rPoly.count() ); + for( sal_Int32 i=0; i<nCount; ++i ) + { + const B2DPoint& rPoint0( rPoly.getB2DPoint(i % nCount) ); + const B2DPoint& rPoint1( rPoly.getB2DPoint((i+1) % nCount) ); + + // is 0 for zero direction vector, 1 for south edge and -1 + // for north edge (standard screen coordinate system) + int nCurrVerticalEdgeType( lcl_sgn( rPoint1.getY() - rPoint0.getY() ) ); + + // is 0 for zero direction vector, 1 for east edge and -1 + // for west edge (standard screen coordinate system) + int nCurrHorizontalEdgeType( lcl_sgn(rPoint1.getX() - rPoint0.getX()) ); + + if( nCurrVerticalEdgeType && nCurrHorizontalEdgeType ) + return false; // oblique edge - for sure no rect + + const bool bCurrNullVertex( !nCurrVerticalEdgeType && !nCurrHorizontalEdgeType ); + + // current vertex is equal to previous - just skip, + // until we have a real edge + if( bCurrNullVertex ) + continue; + + // if previous edge has two identical points, because + // no previous edge direction was available, simply + // take this first non-null edge as the start + // direction. That's what will happen here, if + // bNullVertex is false + if( !bNullVertex ) + { + // 2D cross product - is 1 for CW and -1 for CCW turns + const int nCrossProduct( nHorizontalEdgeType*nCurrVerticalEdgeType - + nVerticalEdgeType*nCurrHorizontalEdgeType ); + + if( !nCrossProduct ) + continue; // no change in orientation - + // collinear edges - just go on + + // if polygon orientation is not set, we'll + // determine it now + if( !bOrientationSet ) + { + bCWPolygon = nCrossProduct == 1; + bOrientationSet = true; + } + else + { + // if current turn orientation is not equal + // initial orientation, this is not a + // rectangle (as rectangles have consistent + // orientation). + if( (nCrossProduct == 1) != bCWPolygon ) + return false; + } + + ++nNumTurns; + + // More than four 90 degree turns are an + // indication that this must not be a rectangle. + if( nNumTurns > 4 ) + return false; + } + + // store current state for the next turn + nVerticalEdgeType = nCurrVerticalEdgeType; + nHorizontalEdgeType = nCurrHorizontalEdgeType; + bNullVertex = false; // won't reach this line, + // if bCurrNullVertex is + // true - see above + } + + return true; + } + + B3DPolygon createB3DPolygonFromB2DPolygon(const B2DPolygon& rCandidate, double fZCoordinate) + { + if(rCandidate.areControlPointsUsed()) + { + // call myself recursively with subdivided input + const B2DPolygon aCandidate(adaptiveSubdivideByAngle(rCandidate)); + return createB3DPolygonFromB2DPolygon(aCandidate, fZCoordinate); + } + else + { + B3DPolygon aRetval; + + for(sal_uInt32 a(0); a < rCandidate.count(); a++) + { + B2DPoint aPoint(rCandidate.getB2DPoint(a)); + aRetval.append(B3DPoint(aPoint.getX(), aPoint.getY(), fZCoordinate)); + } + + // copy closed state + aRetval.setClosed(rCandidate.isClosed()); + + return aRetval; + } + } + + B2DPolygon createB2DPolygonFromB3DPolygon(const B3DPolygon& rCandidate, const B3DHomMatrix& rMat) + { + B2DPolygon aRetval; + const sal_uInt32 nCount(rCandidate.count()); + const bool bIsIdentity(rMat.isIdentity()); + + for(sal_uInt32 a(0); a < nCount; a++) + { + B3DPoint aCandidate(rCandidate.getB3DPoint(a)); + + if(!bIsIdentity) + { + aCandidate *= rMat; + } + + aRetval.append(B2DPoint(aCandidate.getX(), aCandidate.getY())); + } + + // copy closed state + aRetval.setClosed(rCandidate.isClosed()); + + return aRetval; + } + + double getSmallestDistancePointToEdge(const B2DPoint& rPointA, const B2DPoint& rPointB, const B2DPoint& rTestPoint, double& rCut) + { + if(rPointA.equal(rPointB)) + { + rCut = 0.0; + const B2DVector aVector(rTestPoint - rPointA); + return aVector.getLength(); + } + else + { + // get the relative cut value on line vector (Vector1) for cut with perpendicular through TestPoint + const B2DVector aVector1(rPointB - rPointA); + const B2DVector aVector2(rTestPoint - rPointA); + const double fDividend((aVector2.getX() * aVector1.getX()) + (aVector2.getY() * aVector1.getY())); + const double fDivisor((aVector1.getX() * aVector1.getX()) + (aVector1.getY() * aVector1.getY())); + const double fCut(fDividend / fDivisor); + + if(fCut < 0.0) + { + // not in line range, get distance to PointA + rCut = 0.0; + return aVector2.getLength(); + } + else if(fCut > 1.0) + { + // not in line range, get distance to PointB + rCut = 1.0; + const B2DVector aVector(rTestPoint - rPointB); + return aVector.getLength(); + } + else + { + // in line range + const B2DPoint aCutPoint(rPointA + fCut * aVector1); + const B2DVector aVector(rTestPoint - aCutPoint); + rCut = fCut; + return aVector.getLength(); + } + } + } + + double getSmallestDistancePointToPolygon(const B2DPolygon& rCandidate, const B2DPoint& rTestPoint, sal_uInt32& rEdgeIndex, double& rCut) + { + double fRetval(DBL_MAX); + const sal_uInt32 nPointCount(rCandidate.count()); + + if(nPointCount > 1) + { + const double fZero(0.0); + const sal_uInt32 nEdgeCount(rCandidate.isClosed() ? nPointCount : nPointCount - 1); + B2DCubicBezier aBezier; + aBezier.setStartPoint(rCandidate.getB2DPoint(0)); + + for(sal_uInt32 a(0); a < nEdgeCount; a++) + { + const sal_uInt32 nNextIndex((a + 1) % nPointCount); + aBezier.setEndPoint(rCandidate.getB2DPoint(nNextIndex)); + double fEdgeDist; + double fNewCut(0.0); + bool bEdgeIsCurve(false); + + if(rCandidate.areControlPointsUsed()) + { + aBezier.setControlPointA(rCandidate.getNextControlPoint(a)); + aBezier.setControlPointB(rCandidate.getPrevControlPoint(nNextIndex)); + aBezier.testAndSolveTrivialBezier(); + bEdgeIsCurve = aBezier.isBezier(); + } + + if(bEdgeIsCurve) + { + fEdgeDist = aBezier.getSmallestDistancePointToBezierSegment(rTestPoint, fNewCut); + } + else + { + fEdgeDist = getSmallestDistancePointToEdge(aBezier.getStartPoint(), aBezier.getEndPoint(), rTestPoint, fNewCut); + } + + if(fRetval == DBL_MAX || fEdgeDist < fRetval) + { + fRetval = fEdgeDist; + rEdgeIndex = a; + rCut = fNewCut; + + if(fTools::equal(fRetval, fZero)) + { + // already found zero distance, cannot get better. Ensure numerical zero value and end loop. + fRetval = 0.0; + break; + } + } + + // prepare next step + aBezier.setStartPoint(aBezier.getEndPoint()); + } + + if(rtl::math::approxEqual(1.0, rCut)) + { + // correct rEdgeIndex when not last point + if(rCandidate.isClosed()) + { + rEdgeIndex = getIndexOfSuccessor(rEdgeIndex, rCandidate); + rCut = 0.0; + } + else + { + if(rEdgeIndex != nEdgeCount - 1) + { + rEdgeIndex++; + rCut = 0.0; + } + } + } + } + + return fRetval; + } + + B2DPoint distort(const B2DPoint& rCandidate, const B2DRange& rOriginal, const B2DPoint& rTopLeft, const B2DPoint& rTopRight, const B2DPoint& rBottomLeft, const B2DPoint& rBottomRight) + { + if(fTools::equalZero(rOriginal.getWidth()) || fTools::equalZero(rOriginal.getHeight())) + { + return rCandidate; + } + else + { + const double fRelativeX((rCandidate.getX() - rOriginal.getMinX()) / rOriginal.getWidth()); + const double fRelativeY((rCandidate.getY() - rOriginal.getMinY()) / rOriginal.getHeight()); + const double fOneMinusRelativeX(1.0 - fRelativeX); + const double fOneMinusRelativeY(1.0 - fRelativeY); + const double fNewX(fOneMinusRelativeY * (fOneMinusRelativeX * rTopLeft.getX() + fRelativeX * rTopRight.getX()) + + fRelativeY * (fOneMinusRelativeX * rBottomLeft.getX() + fRelativeX * rBottomRight.getX())); + const double fNewY(fOneMinusRelativeX * (fOneMinusRelativeY * rTopLeft.getY() + fRelativeY * rBottomLeft.getY()) + + fRelativeX * (fOneMinusRelativeY * rTopRight.getY() + fRelativeY * rBottomRight.getY())); + + return B2DPoint(fNewX, fNewY); + } + } + + B2DPolygon distort(const B2DPolygon& rCandidate, const B2DRange& rOriginal, const B2DPoint& rTopLeft, const B2DPoint& rTopRight, const B2DPoint& rBottomLeft, const B2DPoint& rBottomRight) + { + const sal_uInt32 nPointCount(rCandidate.count()); + + if(nPointCount && rOriginal.getWidth() != 0.0 && rOriginal.getHeight() != 0.0) + { + B2DPolygon aRetval; + + for(sal_uInt32 a(0); a < nPointCount; a++) + { + aRetval.append(distort(rCandidate.getB2DPoint(a), rOriginal, rTopLeft, rTopRight, rBottomLeft, rBottomRight)); + + if(rCandidate.areControlPointsUsed()) + { + if(!rCandidate.getPrevControlPoint(a).equalZero()) + { + aRetval.setPrevControlPoint(a, distort(rCandidate.getPrevControlPoint(a), rOriginal, rTopLeft, rTopRight, rBottomLeft, rBottomRight)); + } + + if(!rCandidate.getNextControlPoint(a).equalZero()) + { + aRetval.setNextControlPoint(a, distort(rCandidate.getNextControlPoint(a), rOriginal, rTopLeft, rTopRight, rBottomLeft, rBottomRight)); + } + } + } + + aRetval.setClosed(rCandidate.isClosed()); + return aRetval; + } + else + { + return rCandidate; + } + } + + B2DPolygon expandToCurve(const B2DPolygon& rCandidate) + { + B2DPolygon aRetval(rCandidate); + + for(sal_uInt32 a(0); a < rCandidate.count(); a++) + { + expandToCurveInPoint(aRetval, a); + } + + return aRetval; + } + + bool expandToCurveInPoint(B2DPolygon& rCandidate, sal_uInt32 nIndex) + { + OSL_ENSURE(nIndex < rCandidate.count(), "expandToCurveInPoint: Access to polygon out of range (!)"); + bool bRetval(false); + const sal_uInt32 nPointCount(rCandidate.count()); + + if(nPointCount) + { + // predecessor + if(!rCandidate.isPrevControlPointUsed(nIndex)) + { + if(!rCandidate.isClosed() && nIndex == 0) + { + // do not create previous vector for start point of open polygon + } + else + { + const sal_uInt32 nPrevIndex((nIndex + (nPointCount - 1)) % nPointCount); + rCandidate.setPrevControlPoint(nIndex, interpolate(rCandidate.getB2DPoint(nIndex), rCandidate.getB2DPoint(nPrevIndex), 1.0 / 3.0)); + bRetval = true; + } + } + + // successor + if(!rCandidate.isNextControlPointUsed(nIndex)) + { + if(!rCandidate.isClosed() && nIndex + 1 == nPointCount) + { + // do not create next vector for end point of open polygon + } + else + { + const sal_uInt32 nNextIndex((nIndex + 1) % nPointCount); + rCandidate.setNextControlPoint(nIndex, interpolate(rCandidate.getB2DPoint(nIndex), rCandidate.getB2DPoint(nNextIndex), 1.0 / 3.0)); + bRetval = true; + } + } + } + + return bRetval; + } + + bool setContinuityInPoint(B2DPolygon& rCandidate, sal_uInt32 nIndex, B2VectorContinuity eContinuity) + { + OSL_ENSURE(nIndex < rCandidate.count(), "setContinuityInPoint: Access to polygon out of range (!)"); + bool bRetval(false); + const sal_uInt32 nPointCount(rCandidate.count()); + + if(nPointCount) + { + const B2DPoint aCurrentPoint(rCandidate.getB2DPoint(nIndex)); + + switch(eContinuity) + { + case B2VectorContinuity::NONE : + { + if(rCandidate.isPrevControlPointUsed(nIndex)) + { + if(!rCandidate.isClosed() && nIndex == 0) + { + // remove existing previous vector for start point of open polygon + rCandidate.resetPrevControlPoint(nIndex); + } + else + { + const sal_uInt32 nPrevIndex((nIndex + (nPointCount - 1)) % nPointCount); + rCandidate.setPrevControlPoint(nIndex, interpolate(aCurrentPoint, rCandidate.getB2DPoint(nPrevIndex), 1.0 / 3.0)); + } + + bRetval = true; + } + + if(rCandidate.isNextControlPointUsed(nIndex)) + { + if(!rCandidate.isClosed() && nIndex == nPointCount + 1) + { + // remove next vector for end point of open polygon + rCandidate.resetNextControlPoint(nIndex); + } + else + { + const sal_uInt32 nNextIndex((nIndex + 1) % nPointCount); + rCandidate.setNextControlPoint(nIndex, interpolate(aCurrentPoint, rCandidate.getB2DPoint(nNextIndex), 1.0 / 3.0)); + } + + bRetval = true; + } + + break; + } + case B2VectorContinuity::C1 : + { + if(rCandidate.isPrevControlPointUsed(nIndex) && rCandidate.isNextControlPointUsed(nIndex)) + { + // lengths both exist since both are used + B2DVector aVectorPrev(rCandidate.getPrevControlPoint(nIndex) - aCurrentPoint); + B2DVector aVectorNext(rCandidate.getNextControlPoint(nIndex) - aCurrentPoint); + const double fLenPrev(aVectorPrev.getLength()); + const double fLenNext(aVectorNext.getLength()); + aVectorPrev.normalize(); + aVectorNext.normalize(); + const B2VectorOrientation aOrientation(getOrientation(aVectorPrev, aVectorNext)); + + if(aOrientation == B2VectorOrientation::Neutral && aVectorPrev.scalar(aVectorNext) < 0.0) + { + // parallel and opposite direction; check length + if(fTools::equal(fLenPrev, fLenNext)) + { + // this would be even C2, but we want C1. Use the lengths of the corresponding edges. + const sal_uInt32 nPrevIndex((nIndex + (nPointCount - 1)) % nPointCount); + const sal_uInt32 nNextIndex((nIndex + 1) % nPointCount); + const double fLenPrevEdge(B2DVector(rCandidate.getB2DPoint(nPrevIndex) - aCurrentPoint).getLength() * (1.0 / 3.0)); + const double fLenNextEdge(B2DVector(rCandidate.getB2DPoint(nNextIndex) - aCurrentPoint).getLength() * (1.0 / 3.0)); + + rCandidate.setControlPoints(nIndex, + aCurrentPoint + (aVectorPrev * fLenPrevEdge), + aCurrentPoint + (aVectorNext * fLenNextEdge)); + bRetval = true; + } + } + else + { + // not parallel or same direction, set vectors and length + const B2DVector aNormalizedPerpendicular(getNormalizedPerpendicular(aVectorPrev + aVectorNext)); + + if(aOrientation == B2VectorOrientation::Positive) + { + rCandidate.setControlPoints(nIndex, + aCurrentPoint - (aNormalizedPerpendicular * fLenPrev), + aCurrentPoint + (aNormalizedPerpendicular * fLenNext)); + } + else + { + rCandidate.setControlPoints(nIndex, + aCurrentPoint + (aNormalizedPerpendicular * fLenPrev), + aCurrentPoint - (aNormalizedPerpendicular * fLenNext)); + } + + bRetval = true; + } + } + break; + } + case B2VectorContinuity::C2 : + { + if(rCandidate.isPrevControlPointUsed(nIndex) && rCandidate.isNextControlPointUsed(nIndex)) + { + // lengths both exist since both are used + B2DVector aVectorPrev(rCandidate.getPrevControlPoint(nIndex) - aCurrentPoint); + B2DVector aVectorNext(rCandidate.getNextControlPoint(nIndex) - aCurrentPoint); + const double fCommonLength((aVectorPrev.getLength() + aVectorNext.getLength()) / 2.0); + aVectorPrev.normalize(); + aVectorNext.normalize(); + const B2VectorOrientation aOrientation(getOrientation(aVectorPrev, aVectorNext)); + + if(aOrientation == B2VectorOrientation::Neutral && aVectorPrev.scalar(aVectorNext) < 0.0) + { + // parallel and opposite direction; set length. Use one direction for better numerical correctness + const B2DVector aScaledDirection(aVectorPrev * fCommonLength); + + rCandidate.setControlPoints(nIndex, + aCurrentPoint + aScaledDirection, + aCurrentPoint - aScaledDirection); + } + else + { + // not parallel or same direction, set vectors and length + const B2DVector aNormalizedPerpendicular(getNormalizedPerpendicular(aVectorPrev + aVectorNext)); + const B2DVector aPerpendicular(aNormalizedPerpendicular * fCommonLength); + + if(aOrientation == B2VectorOrientation::Positive) + { + rCandidate.setControlPoints(nIndex, + aCurrentPoint - aPerpendicular, + aCurrentPoint + aPerpendicular); + } + else + { + rCandidate.setControlPoints(nIndex, + aCurrentPoint + aPerpendicular, + aCurrentPoint - aPerpendicular); + } + } + + bRetval = true; + } + break; + } + } + } + + return bRetval; + } + + B2DPolygon growInNormalDirection(const B2DPolygon& rCandidate, double fValue) + { + if(fValue != 0.0) + { + if(rCandidate.areControlPointsUsed()) + { + // call myself recursively with subdivided input + const B2DPolygon aCandidate(adaptiveSubdivideByAngle(rCandidate)); + return growInNormalDirection(aCandidate, fValue); + } + else + { + B2DPolygon aRetval; + const sal_uInt32 nPointCount(rCandidate.count()); + + if(nPointCount) + { + B2DPoint aPrev(rCandidate.getB2DPoint(nPointCount - 1)); + B2DPoint aCurrent(rCandidate.getB2DPoint(0)); + + for(sal_uInt32 a(0); a < nPointCount; a++) + { + const B2DPoint aNext(rCandidate.getB2DPoint(a + 1 == nPointCount ? 0 : a + 1)); + const B2DVector aBack(aPrev - aCurrent); + const B2DVector aForw(aNext - aCurrent); + const B2DVector aPerpBack(getNormalizedPerpendicular(aBack)); + const B2DVector aPerpForw(getNormalizedPerpendicular(aForw)); + B2DVector aDirection(aPerpBack - aPerpForw); + aDirection.normalize(); + aDirection *= fValue; + aRetval.append(aCurrent + aDirection); + + // prepare next step + aPrev = aCurrent; + aCurrent = aNext; + } + } + + // copy closed state + aRetval.setClosed(rCandidate.isClosed()); + + return aRetval; + } + } + else + { + return rCandidate; + } + } + + B2DPolygon reSegmentPolygon(const B2DPolygon& rCandidate, sal_uInt32 nSegments) + { + B2DPolygon aRetval; + const sal_uInt32 nPointCount(rCandidate.count()); + + if(nPointCount && nSegments) + { + // get current segment count + const sal_uInt32 nSegmentCount(rCandidate.isClosed() ? nPointCount : nPointCount - 1); + + if(nSegmentCount == nSegments) + { + aRetval = rCandidate; + } + else + { + const double fLength(getLength(rCandidate)); + const sal_uInt32 nLoopCount(rCandidate.isClosed() ? nSegments : nSegments + 1); + + for(sal_uInt32 a(0); a < nLoopCount; a++) + { + const double fRelativePos(static_cast<double>(a) / static_cast<double>(nSegments)); // 0.0 .. 1.0 + const B2DPoint aNewPoint(getPositionRelative(rCandidate, fRelativePos, fLength)); + aRetval.append(aNewPoint); + } + + // copy closed flag + aRetval.setClosed(rCandidate.isClosed()); + } + } + + return aRetval; + } + + B2DPolygon interpolate(const B2DPolygon& rOld1, const B2DPolygon& rOld2, double t) + { + OSL_ENSURE(rOld1.count() == rOld2.count(), "B2DPolygon interpolate: Different geometry (!)"); + + if(fTools::lessOrEqual(t, 0.0) || rOld1 == rOld2) + { + return rOld1; + } + else if(fTools::moreOrEqual(t, 1.0)) + { + return rOld2; + } + else + { + B2DPolygon aRetval; + const bool bInterpolateVectors(rOld1.areControlPointsUsed() || rOld2.areControlPointsUsed()); + aRetval.setClosed(rOld1.isClosed() && rOld2.isClosed()); + + for(sal_uInt32 a(0); a < rOld1.count(); a++) + { + aRetval.append(interpolate(rOld1.getB2DPoint(a), rOld2.getB2DPoint(a), t)); + + if(bInterpolateVectors) + { + aRetval.setPrevControlPoint(a, interpolate(rOld1.getPrevControlPoint(a), rOld2.getPrevControlPoint(a), t)); + aRetval.setNextControlPoint(a, interpolate(rOld1.getNextControlPoint(a), rOld2.getNextControlPoint(a), t)); + } + } + + return aRetval; + } + } + + // #i76891# + B2DPolygon simplifyCurveSegments(const B2DPolygon& rCandidate) + { + const sal_uInt32 nPointCount(rCandidate.count()); + + if(nPointCount && rCandidate.areControlPointsUsed()) + { + // prepare loop + const sal_uInt32 nEdgeCount(rCandidate.isClosed() ? nPointCount : nPointCount - 1); + B2DPolygon aRetval; + B2DCubicBezier aBezier; + aBezier.setStartPoint(rCandidate.getB2DPoint(0)); + + // try to avoid costly reallocations + aRetval.reserve( nEdgeCount+1); + + // add start point + aRetval.append(aBezier.getStartPoint()); + + for(sal_uInt32 a(0); a < nEdgeCount; a++) + { + // get values for edge + const sal_uInt32 nNextIndex((a + 1) % nPointCount); + aBezier.setEndPoint(rCandidate.getB2DPoint(nNextIndex)); + aBezier.setControlPointA(rCandidate.getNextControlPoint(a)); + aBezier.setControlPointB(rCandidate.getPrevControlPoint(nNextIndex)); + aBezier.testAndSolveTrivialBezier(); + + // still bezier? + if(aBezier.isBezier()) + { + // add edge with control vectors + aRetval.appendBezierSegment(aBezier.getControlPointA(), aBezier.getControlPointB(), aBezier.getEndPoint()); + } + else + { + // add edge + aRetval.append(aBezier.getEndPoint()); + } + + // next point + aBezier.setStartPoint(aBezier.getEndPoint()); + } + + if(rCandidate.isClosed()) + { + // set closed flag, rescue control point and correct last double point + closeWithGeometryChange(aRetval); + } + + return aRetval; + } + else + { + return rCandidate; + } + } + + // makes the given indexed point the new polygon start point. To do that, the points in the + // polygon will be rotated. This is only valid for closed polygons, for non-closed ones + // an assertion will be triggered + B2DPolygon makeStartPoint(const B2DPolygon& rCandidate, sal_uInt32 nIndexOfNewStatPoint) + { + const sal_uInt32 nPointCount(rCandidate.count()); + + if(nPointCount > 2 && nIndexOfNewStatPoint != 0 && nIndexOfNewStatPoint < nPointCount) + { + OSL_ENSURE(rCandidate.isClosed(), "makeStartPoint: only valid for closed polygons (!)"); + B2DPolygon aRetval; + + for(sal_uInt32 a(0); a < nPointCount; a++) + { + const sal_uInt32 nSourceIndex((a + nIndexOfNewStatPoint) % nPointCount); + aRetval.append(rCandidate.getB2DPoint(nSourceIndex)); + + if(rCandidate.areControlPointsUsed()) + { + aRetval.setPrevControlPoint(a, rCandidate.getPrevControlPoint(nSourceIndex)); + aRetval.setNextControlPoint(a, rCandidate.getNextControlPoint(nSourceIndex)); + } + } + + return aRetval; + } + + return rCandidate; + } + + B2DPolygon createEdgesOfGivenLength(const B2DPolygon& rCandidate, double fLength, double fStart, double fEnd) + { + B2DPolygon aRetval; + + if(fLength < 0.0) + { + fLength = 0.0; + } + + if(!fTools::equalZero(fLength)) + { + if(fStart < 0.0) + { + fStart = 0.0; + } + + if(fEnd < 0.0) + { + fEnd = 0.0; + } + + if(fEnd < fStart) + { + fEnd = fStart; + } + + // iterate and consume pieces with fLength. First subdivide to reduce input to line segments + const B2DPolygon aCandidate(rCandidate.areControlPointsUsed() ? rCandidate.getDefaultAdaptiveSubdivision() : rCandidate); + const sal_uInt32 nPointCount(aCandidate.count()); + + if(nPointCount > 1) + { + const bool bEndActive(!fTools::equalZero(fEnd)); + const sal_uInt32 nEdgeCount(aCandidate.isClosed() ? nPointCount : nPointCount - 1); + B2DPoint aCurrent(aCandidate.getB2DPoint(0)); + double fPositionInEdge(fStart); + double fAbsolutePosition(fStart); + + for(sal_uInt32 a(0); a < nEdgeCount; a++) + { + const sal_uInt32 nNextIndex((a + 1) % nPointCount); + const B2DPoint aNext(aCandidate.getB2DPoint(nNextIndex)); + const B2DVector aEdge(aNext - aCurrent); + double fEdgeLength(aEdge.getLength()); + + if(!fTools::equalZero(fEdgeLength)) + { + while(fTools::less(fPositionInEdge, fEdgeLength)) + { + // move position on edge forward as long as on edge + const double fScalar(fPositionInEdge / fEdgeLength); + aRetval.append(aCurrent + (aEdge * fScalar)); + fPositionInEdge += fLength; + + if(bEndActive) + { + fAbsolutePosition += fLength; + + if(fTools::more(fAbsolutePosition, fEnd)) + { + break; + } + } + } + + // subtract length of current edge + fPositionInEdge -= fEdgeLength; + } + + if(bEndActive && fTools::more(fAbsolutePosition, fEnd)) + { + break; + } + + // prepare next step + aCurrent = aNext; + } + + // keep closed state + aRetval.setClosed(aCandidate.isClosed()); + } + else + { + // source polygon has only one point, return unchanged + aRetval = aCandidate; + } + } + + return aRetval; + } + + B2DPolygon createWaveline(const B2DPolygon& rCandidate, double fWaveWidth, double fWaveHeight) + { + B2DPolygon aRetval; + + if(fWaveWidth < 0.0) + { + fWaveWidth = 0.0; + } + + if(fWaveHeight < 0.0) + { + fWaveHeight = 0.0; + } + + const bool bHasWidth(!fTools::equalZero(fWaveWidth)); + + if(bHasWidth) + { + const bool bHasHeight(!fTools::equalZero(fWaveHeight)); + if(bHasHeight) + { + // width and height, create waveline. First subdivide to reduce input to line segments + // of WaveWidth. Last segment may be missing. If this turns out to be a problem, it + // may be added here again using the original last point from rCandidate. It may + // also be the case that rCandidate was closed. To simplify things it is handled here + // as if it was opened. + // Result from createEdgesOfGivenLength contains no curved segments, handle as straight + // edges. + const B2DPolygon aEqualLenghEdges(createEdgesOfGivenLength(rCandidate, fWaveWidth)); + const sal_uInt32 nPointCount(aEqualLenghEdges.count()); + + if(nPointCount > 1) + { + // iterate over straight edges, add start point + B2DPoint aCurrent(aEqualLenghEdges.getB2DPoint(0)); + aRetval.append(aCurrent); + + for(sal_uInt32 a(0); a < nPointCount - 1; a++) + { + const sal_uInt32 nNextIndex((a + 1) % nPointCount); + const B2DPoint aNext(aEqualLenghEdges.getB2DPoint(nNextIndex)); + const B2DVector aEdge(aNext - aCurrent); + const B2DVector aPerpendicular(getNormalizedPerpendicular(aEdge)); + const B2DVector aControlOffset((aEdge * 0.467308) - (aPerpendicular * fWaveHeight)); + + // add curve segment + aRetval.appendBezierSegment( + aCurrent + aControlOffset, + aNext - aControlOffset, + aNext); + + // prepare next step + aCurrent = aNext; + } + } + } + else + { + // width but no height -> return original polygon + aRetval = rCandidate; + } + } + else + { + // no width -> no waveline, stay empty and return + } + + return aRetval; + } + + // snap points of horizontal or vertical edges to discrete values + B2DPolygon snapPointsOfHorizontalOrVerticalEdges(const B2DPolygon& rCandidate) + { + const sal_uInt32 nPointCount(rCandidate.count()); + + if(nPointCount > 1) + { + // Start by copying the source polygon to get a writeable copy. The closed state is + // copied by aRetval's initialisation, too, so no need to copy it in this method + B2DPolygon aRetval(rCandidate); + + // prepare geometry data. Get rounded from original + B2ITuple aPrevTuple(basegfx::fround(rCandidate.getB2DPoint(nPointCount - 1))); + B2DPoint aCurrPoint(rCandidate.getB2DPoint(0)); + B2ITuple aCurrTuple(basegfx::fround(aCurrPoint)); + + // loop over all points. This will also snap the implicit closing edge + // even when not closed, but that's no problem here + for(sal_uInt32 a(0); a < nPointCount; a++) + { + // get next point. Get rounded from original + const bool bLastRun(a + 1 == nPointCount); + const sal_uInt32 nNextIndex(bLastRun ? 0 : a + 1); + const B2DPoint aNextPoint(rCandidate.getB2DPoint(nNextIndex)); + const B2ITuple aNextTuple(basegfx::fround(aNextPoint)); + + // get the states + const bool bPrevVertical(aPrevTuple.getX() == aCurrTuple.getX()); + const bool bNextVertical(aNextTuple.getX() == aCurrTuple.getX()); + const bool bPrevHorizontal(aPrevTuple.getY() == aCurrTuple.getY()); + const bool bNextHorizontal(aNextTuple.getY() == aCurrTuple.getY()); + const bool bSnapX(bPrevVertical || bNextVertical); + const bool bSnapY(bPrevHorizontal || bNextHorizontal); + + if(bSnapX || bSnapY) + { + const B2DPoint aSnappedPoint( + bSnapX ? aCurrTuple.getX() : aCurrPoint.getX(), + bSnapY ? aCurrTuple.getY() : aCurrPoint.getY()); + + aRetval.setB2DPoint(a, aSnappedPoint); + } + + // prepare next point + if(!bLastRun) + { + aPrevTuple = aCurrTuple; + aCurrPoint = aNextPoint; + aCurrTuple = aNextTuple; + } + } + + return aRetval; + } + else + { + return rCandidate; + } + } + + B2DVector getTangentEnteringPoint(const B2DPolygon& rCandidate, sal_uInt32 nIndex) + { + B2DVector aRetval(0.0, 0.0); + const sal_uInt32 nCount(rCandidate.count()); + + if(nIndex >= nCount) + { + // out of range + return aRetval; + } + + // start immediately at prev point compared to nIndex + const bool bClosed(rCandidate.isClosed()); + sal_uInt32 nPrev(bClosed ? (nIndex + nCount - 1) % nCount : nIndex ? nIndex - 1 : nIndex); + + if(nPrev == nIndex) + { + // no previous, done + return aRetval; + } + + B2DCubicBezier aSegment; + + // go backward in the polygon; if closed, maximal back to start index (nIndex); if not closed, + // until zero. Use nIndex as stop criteria + while(nPrev != nIndex) + { + // get BezierSegment and tangent at the *end* of segment + rCandidate.getBezierSegment(nPrev, aSegment); + aRetval = aSegment.getTangent(1.0); + + if(!aRetval.equalZero()) + { + // if we have a tangent, return it + return aRetval; + } + + // prepare index before checked one + nPrev = bClosed ? (nPrev + nCount - 1) % nCount : nPrev ? nPrev - 1 : nIndex; + } + + return aRetval; + } + + B2DVector getTangentLeavingPoint(const B2DPolygon& rCandidate, sal_uInt32 nIndex) + { + B2DVector aRetval(0.0, 0.0); + const sal_uInt32 nCount(rCandidate.count()); + + if(nIndex >= nCount) + { + // out of range + return aRetval; + } + + // start at nIndex + const bool bClosed(rCandidate.isClosed()); + sal_uInt32 nCurrent(nIndex); + B2DCubicBezier aSegment; + + // go forward; if closed, do this until once around and back at start index (nIndex); if not + // closed, until last point (nCount - 1). Use nIndex as stop criteria + do + { + // get BezierSegment and tangent at the *beginning* of segment + rCandidate.getBezierSegment(nCurrent, aSegment); + aRetval = aSegment.getTangent(0.0); + + if(!aRetval.equalZero()) + { + // if we have a tangent, return it + return aRetval; + } + + // prepare next index + nCurrent = bClosed ? (nCurrent + 1) % nCount : nCurrent + 1 < nCount ? nCurrent + 1 : nIndex; + } + while(nCurrent != nIndex); + + return aRetval; + } + + // converters for css::drawing::PointSequence + + B2DPolygon UnoPointSequenceToB2DPolygon( + const css::drawing::PointSequence& rPointSequenceSource) + { + B2DPolygon aRetval; + const sal_uInt32 nLength(rPointSequenceSource.getLength()); + + if(nLength) + { + aRetval.reserve(nLength); + const css::awt::Point* pArray = rPointSequenceSource.getConstArray(); + const css::awt::Point* pArrayEnd = pArray + rPointSequenceSource.getLength(); + + for(;pArray != pArrayEnd; pArray++) + { + aRetval.append(B2DPoint(pArray->X, pArray->Y)); + } + + // check for closed state flag + utils::checkClosed(aRetval); + } + + return aRetval; + } + + void B2DPolygonToUnoPointSequence( + const B2DPolygon& rPolygon, + css::drawing::PointSequence& rPointSequenceRetval) + { + B2DPolygon aPolygon(rPolygon); + + if(aPolygon.areControlPointsUsed()) + { + OSL_ENSURE(false, "B2DPolygonToUnoPointSequence: Source contains bezier segments, wrong UNO API data type may be used (!)"); + aPolygon = aPolygon.getDefaultAdaptiveSubdivision(); + } + + const sal_uInt32 nPointCount(aPolygon.count()); + + if(nPointCount) + { + // Take closed state into account, the API polygon still uses the old closed definition + // with last/first point are identical (cannot hold information about open polygons with identical + // first and last point, though) + const bool bIsClosed(aPolygon.isClosed()); + + rPointSequenceRetval.realloc(bIsClosed ? nPointCount + 1 : nPointCount); + css::awt::Point* pSequence = rPointSequenceRetval.getArray(); + + for(sal_uInt32 b(0); b < nPointCount; b++) + { + const B2DPoint aPoint(aPolygon.getB2DPoint(b)); + const css::awt::Point aAPIPoint(fround(aPoint.getX()), fround(aPoint.getY())); + + *pSequence = aAPIPoint; + pSequence++; + } + + // copy first point if closed + if(bIsClosed) + { + *pSequence = *rPointSequenceRetval.getConstArray(); + } + } + else + { + rPointSequenceRetval.realloc(0); + } + } + + // converters for css::drawing::PointSequence and + // css::drawing::FlagSequence to B2DPolygon (curved polygons) + + B2DPolygon UnoPolygonBezierCoordsToB2DPolygon( + const css::drawing::PointSequence& rPointSequenceSource, + const css::drawing::FlagSequence& rFlagSequenceSource) + { + const sal_uInt32 nCount(static_cast<sal_uInt32>(rPointSequenceSource.getLength())); + OSL_ENSURE(nCount == static_cast<sal_uInt32>(rFlagSequenceSource.getLength()), + "UnoPolygonBezierCoordsToB2DPolygon: Unequal count of Points and Flags (!)"); + + // prepare new polygon + B2DPolygon aRetval; + + if(0 != nCount) + { + aRetval.reserve(nCount); + + const css::awt::Point* pPointSequence = rPointSequenceSource.getConstArray(); + const css::drawing::PolygonFlags* pFlagSequence = rFlagSequenceSource.getConstArray(); + + // get first point and flag + B2DPoint aNewCoordinatePair(pPointSequence->X, pPointSequence->Y); pPointSequence++; + css::drawing::PolygonFlags ePolygonFlag(*pFlagSequence); pFlagSequence++; + B2DPoint aControlA; + B2DPoint aControlB; + + // first point is not allowed to be a control point + OSL_ENSURE(ePolygonFlag != css::drawing::PolygonFlags_CONTROL, + "UnoPolygonBezierCoordsToB2DPolygon: Start point is a control point, illegal input polygon (!)"); + + // add first point as start point + aRetval.append(aNewCoordinatePair); + + for(sal_uInt32 b(1); b < nCount;) + { + // prepare loop + bool bControlA(false); + bool bControlB(false); + + // get next point and flag + aNewCoordinatePair = B2DPoint(pPointSequence->X, pPointSequence->Y); + ePolygonFlag = *pFlagSequence; + pPointSequence++; pFlagSequence++; b++; + + if(b < nCount && ePolygonFlag == css::drawing::PolygonFlags_CONTROL) + { + aControlA = aNewCoordinatePair; + bControlA = true; + + // get next point and flag + aNewCoordinatePair = B2DPoint(pPointSequence->X, pPointSequence->Y); + ePolygonFlag = *pFlagSequence; + pPointSequence++; pFlagSequence++; b++; + } + + if(b < nCount && ePolygonFlag == css::drawing::PolygonFlags_CONTROL) + { + aControlB = aNewCoordinatePair; + bControlB = true; + + // get next point and flag + aNewCoordinatePair = B2DPoint(pPointSequence->X, pPointSequence->Y); + ePolygonFlag = *pFlagSequence; + pPointSequence++; pFlagSequence++; b++; + } + + // two or no control points are consumed, another one would be an error. + // It's also an error if only one control point was read + SAL_WARN_IF(ePolygonFlag == css::drawing::PolygonFlags_CONTROL || bControlA != bControlB, + "basegfx", "UnoPolygonBezierCoordsToB2DPolygon: Illegal source polygon (!)"); + + // the previous writes used the B2DPolyPolygon -> utils::PolyPolygon converter + // which did not create minimal PolyPolygons, but created all control points + // as null vectors (identical points). Because of the former P(CA)(CB)-norm of + // B2DPolygon and it's unused sign of being the zero-vector and CA and CB being + // relative to P, an empty edge was exported as P == CA == CB. Luckily, the new + // export format can be read without errors by the old OOo-versions, so we need only + // to correct here at read and do not need to export a wrong but compatible version + // for the future. + if(bControlA + && aControlA.equal(aControlB) + && aControlA.equal(aRetval.getB2DPoint(aRetval.count() - 1))) + { + bControlA = false; + } + + if(bControlA) + { + // add bezier edge + aRetval.appendBezierSegment(aControlA, aControlB, aNewCoordinatePair); + } + else + { + // add edge + aRetval.append(aNewCoordinatePair); + } + } + + // #i72807# API import uses old line start/end-equal definition for closed, + // so we need to correct this to closed state here + checkClosed(aRetval); + } + + return aRetval; + } + + void B2DPolygonToUnoPolygonBezierCoords( + const B2DPolygon& rPolygon, + css::drawing::PointSequence& rPointSequenceRetval, + css::drawing::FlagSequence& rFlagSequenceRetval) + { + const sal_uInt32 nPointCount(rPolygon.count()); + + if(nPointCount) + { + const bool bCurve(rPolygon.areControlPointsUsed()); + const bool bClosed(rPolygon.isClosed()); + + if(bCurve) + { + // calculate target point count + const sal_uInt32 nLoopCount(bClosed ? nPointCount : nPointCount - 1); + + if(nLoopCount) + { + // prepare target data. The real needed number of target points (and flags) + // could only be calculated by using two loops, so use dynamic memory + std::vector< css::awt::Point > aCollectPoints; + std::vector< css::drawing::PolygonFlags > aCollectFlags; + + // reserve maximum creatable points + const sal_uInt32 nMaxTargetCount((nLoopCount * 3) + 1); + aCollectPoints.reserve(nMaxTargetCount); + aCollectFlags.reserve(nMaxTargetCount); + + // prepare current bezier segment by setting start point + B2DCubicBezier aBezierSegment; + aBezierSegment.setStartPoint(rPolygon.getB2DPoint(0)); + + for(sal_uInt32 a(0); a < nLoopCount; a++) + { + // add current point (always) and remember StartPointIndex for evtl. later corrections + const sal_uInt32 nStartPointIndex(aCollectPoints.size()); + aCollectPoints.emplace_back( + fround(aBezierSegment.getStartPoint().getX()), + fround(aBezierSegment.getStartPoint().getY())); + aCollectFlags.push_back(css::drawing::PolygonFlags_NORMAL); + + // prepare next segment + const sal_uInt32 nNextIndex((a + 1) % nPointCount); + aBezierSegment.setEndPoint(rPolygon.getB2DPoint(nNextIndex)); + aBezierSegment.setControlPointA(rPolygon.getNextControlPoint(a)); + aBezierSegment.setControlPointB(rPolygon.getPrevControlPoint(nNextIndex)); + + if(aBezierSegment.isBezier()) + { + // if bezier is used, add always two control points due to the old schema + aCollectPoints.emplace_back( + fround(aBezierSegment.getControlPointA().getX()), + fround(aBezierSegment.getControlPointA().getY())); + aCollectFlags.push_back(css::drawing::PolygonFlags_CONTROL); + + aCollectPoints.emplace_back( + fround(aBezierSegment.getControlPointB().getX()), + fround(aBezierSegment.getControlPointB().getY())); + aCollectFlags.push_back(css::drawing::PolygonFlags_CONTROL); + } + + // test continuity with previous control point to set flag value + if(aBezierSegment.getControlPointA() != aBezierSegment.getStartPoint() && (bClosed || a)) + { + const B2VectorContinuity eCont(rPolygon.getContinuityInPoint(a)); + + if(eCont == B2VectorContinuity::C1) + { + aCollectFlags[nStartPointIndex] = css::drawing::PolygonFlags_SMOOTH; + } + else if(eCont == B2VectorContinuity::C2) + { + aCollectFlags[nStartPointIndex] = css::drawing::PolygonFlags_SYMMETRIC; + } + } + + // prepare next loop + aBezierSegment.setStartPoint(aBezierSegment.getEndPoint()); + } + + if(bClosed) + { + // add first point again as closing point due to old definition + aCollectPoints.push_back(aCollectPoints[0]); + aCollectFlags.push_back(css::drawing::PolygonFlags_NORMAL); + } + else + { + // add last point as closing point + const B2DPoint aClosingPoint(rPolygon.getB2DPoint(nPointCount - 1)); + aCollectPoints.emplace_back( + fround(aClosingPoint.getX()), + fround(aClosingPoint.getY())); + aCollectFlags.push_back(css::drawing::PolygonFlags_NORMAL); + } + + // copy collected data to target arrays + const sal_uInt32 nTargetCount(aCollectPoints.size()); + OSL_ENSURE(nTargetCount == aCollectFlags.size(), "Unequal Point and Flag count (!)"); + + rPointSequenceRetval.realloc(static_cast<sal_Int32>(nTargetCount)); + rFlagSequenceRetval.realloc(static_cast<sal_Int32>(nTargetCount)); + css::awt::Point* pPointSequence = rPointSequenceRetval.getArray(); + css::drawing::PolygonFlags* pFlagSequence = rFlagSequenceRetval.getArray(); + + for(sal_uInt32 a(0); a < nTargetCount; a++) + { + *pPointSequence = aCollectPoints[a]; + *pFlagSequence = aCollectFlags[a]; + pPointSequence++; + pFlagSequence++; + } + } + } + else + { + // straightforward point list creation + const sal_uInt32 nTargetCount(nPointCount + (bClosed ? 1 : 0)); + + rPointSequenceRetval.realloc(static_cast<sal_Int32>(nTargetCount)); + rFlagSequenceRetval.realloc(static_cast<sal_Int32>(nTargetCount)); + + css::awt::Point* pPointSequence = rPointSequenceRetval.getArray(); + css::drawing::PolygonFlags* pFlagSequence = rFlagSequenceRetval.getArray(); + + for(sal_uInt32 a(0); a < nPointCount; a++) + { + const B2DPoint aB2DPoint(rPolygon.getB2DPoint(a)); + const css::awt::Point aAPIPoint( + fround(aB2DPoint.getX()), + fround(aB2DPoint.getY())); + + *pPointSequence = aAPIPoint; + *pFlagSequence = css::drawing::PolygonFlags_NORMAL; + pPointSequence++; + pFlagSequence++; + } + + if(bClosed) + { + // add first point as closing point + *pPointSequence = *rPointSequenceRetval.getConstArray(); + *pFlagSequence = css::drawing::PolygonFlags_NORMAL; + } + } + } + else + { + rPointSequenceRetval.realloc(0); + rFlagSequenceRetval.realloc(0); + } + } + +B2DPolygon createSimplifiedPolygon(const B2DPolygon& rCandidate, const double fTolerance) +{ + const sal_uInt32 nPointCount(rCandidate.count()); + if (nPointCount < 3 || rCandidate.areControlPointsUsed()) + return rCandidate; + + // The solution does not use recursion directly, since this could lead to a stack overflow if + // nPointCount is very large. Instead, an own stack is used. This does not contain points, but + // pairs of low and high index of a range in rCandidate. A parallel boolean vector is used to note + // whether a point of rCandidate belongs to the output polygon or not. + std::vector<bool> bIsKeptVec(nPointCount, false); + bIsKeptVec[0] = true; + bIsKeptVec[nPointCount - 1] = true; + sal_uInt32 nKept = 2; + std::stack<std::pair<sal_uInt32, sal_uInt32>> aUnfinishedRangesStack; + + // The RDP algorithm draws a straight line from the first point to the last point of a range. + // Then, from the inner points of the range, the point that has the largest distance to the line + // is determined. If the distance is greater than the tolerance, this point is kept and the lower + // and upper sub-segments of the range are treated in the same way. If the distance is smaller + // than the tolerance, none of the inner points will be kept. + sal_uInt32 nLowIndex = 0; + sal_uInt32 nHighIndex = nPointCount - 1; + bool bContinue = true; + do + { + bContinue = false; + if (nHighIndex - nLowIndex < 2) // maximal two points, range is finished. + { + // continue with sibling upper range if any + if (!aUnfinishedRangesStack.empty()) + { + std::pair<sal_uInt32, sal_uInt32> aIndexPair = aUnfinishedRangesStack.top(); + aUnfinishedRangesStack.pop(); + nLowIndex = aIndexPair.first; + nHighIndex = aIndexPair.second; + bContinue = true; + } + } + else // the range needs examine the max distance + { + // Get maximal distance of inner points of the range to the straight line from start to + // end point of the range. + // For calculation of the distance we use the Hesse normal form of the straight line. + B2DPoint aLowPoint = rCandidate.getB2DPoint(nLowIndex); + B2DPoint aHighPoint = rCandidate.getB2DPoint(nHighIndex); + B2DVector aNormalVec + = basegfx::getNormalizedPerpendicular(B2DVector(aHighPoint) - B2DVector(aLowPoint)); + double fLineOriginDistance = aNormalVec.scalar(B2DVector(aLowPoint)); + double fMaxDist = 0; + sal_uInt32 nMaxPointIndex = nLowIndex; + for (sal_uInt32 i = nLowIndex + 1; i < nHighIndex; i++) + { + double fDistance + = aNormalVec.scalar(B2DVector(rCandidate.getB2DPoint(i))) - fLineOriginDistance; + if (std::fabs(fDistance) > fMaxDist) + { + fMaxDist = std::fabs(fDistance); + nMaxPointIndex = i; + } + } + + if (fMaxDist >= fTolerance) + { + // We need to divide the current range into two sub ranges. + bIsKeptVec[nMaxPointIndex] = true; + nKept++; + // continue with lower sub range and push upper sub range to stack + aUnfinishedRangesStack.push(std::make_pair(nMaxPointIndex, nHighIndex)); + nHighIndex = nMaxPointIndex; + bContinue = true; + } + else + { + // We do not keep any of the inner points of the current range. + // continue with sibling upper range if any + if (!aUnfinishedRangesStack.empty()) + { + std::pair<sal_uInt32, sal_uInt32> aIndexPair = aUnfinishedRangesStack.top(); + aUnfinishedRangesStack.pop(); + nLowIndex = aIndexPair.first; + nHighIndex = aIndexPair.second; + bContinue = true; + } + } + } + } while (bContinue); + + // Put all points which are marked as "keep" into the result polygon + B2DPolygon aResultPolygon; + aResultPolygon.reserve(nKept); + for (sal_uInt32 i = 0; i < nPointCount; i++) + { + if (bIsKeptVec[i]) + aResultPolygon.append(rCandidate.getB2DPoint(i)); + } + return aResultPolygon; +} + +} // end of namespace + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/basegfx/source/polygon/b2dpolygontriangulator.cxx b/basegfx/source/polygon/b2dpolygontriangulator.cxx new file mode 100644 index 0000000000..8742ade70e --- /dev/null +++ b/basegfx/source/polygon/b2dpolygontriangulator.cxx @@ -0,0 +1,434 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * 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 . + */ + +#include <basegfx/polygon/b2dpolygontriangulator.hxx> +#include <basegfx/point/b2dpoint.hxx> +#include <basegfx/polygon/b2dpolygon.hxx> +#include <basegfx/polygon/b2dpolypolygon.hxx> +#include <basegfx/vector/b2dvector.hxx> +#include <basegfx/polygon/b2dpolygontools.hxx> +#include <basegfx/polygon/b2dpolypolygontools.hxx> +#include <basegfx/range/b2drange.hxx> +#include <basegfx/numeric/ftools.hxx> + +#include <algorithm> + +namespace basegfx +{ + namespace + { + class EdgeEntry + { + EdgeEntry* mpNext; + B2DPoint maStart; + B2DPoint maEnd; + double mfAtan2; + + public: + EdgeEntry(const B2DPoint& rStart, const B2DPoint& rEnd) + : mpNext(nullptr), + maStart(rStart), + maEnd(rEnd), + mfAtan2(0.0) + { + // make sure edge goes down. If horizontal, let it go to the right (left-handed). + bool bSwap(false); + + if(::basegfx::fTools::equal(maStart.getY(), maEnd.getY())) + { + if(maStart.getX() > maEnd.getX()) + { + bSwap = true; + } + } + else if(maStart.getY() > maEnd.getY()) + { + bSwap = true; + } + + if(bSwap) + { + maStart = rEnd; + maEnd = rStart; + } + + mfAtan2 = atan2(maEnd.getY() - maStart.getY(), maEnd.getX() - maStart.getX()); + } + + bool operator<(const EdgeEntry& rComp) const + { + if(::basegfx::fTools::equal(maStart.getY(), rComp.maStart.getY())) + { + if(::basegfx::fTools::equal(maStart.getX(), rComp.maStart.getX())) + { + // same in x and y -> same start point. Sort emitting vectors from left to right. + return (mfAtan2 > rComp.mfAtan2); + } + + return (maStart.getX() < rComp.maStart.getX()); + } + + return (maStart.getY() < rComp.maStart.getY()); + } + + bool operator==(const EdgeEntry& rComp) const + { + return (maStart.equal(rComp.maStart) && maEnd.equal(rComp.maEnd)); + } + + bool operator!=(const EdgeEntry& rComp) const + { + return !(*this == rComp); + } + + const B2DPoint& getStart() const { return maStart; } + const B2DPoint& getEnd() const { return maEnd; } + + EdgeEntry* getNext() const { return mpNext; } + void setNext(EdgeEntry* pNext) { mpNext = pNext; } + }; + + typedef std::vector< EdgeEntry > EdgeEntries; + + class Triangulator + { + EdgeEntry* mpList; + EdgeEntries maStartEntries; + std::vector< std::unique_ptr<EdgeEntry> > maNewEdgeEntries; + triangulator::B2DTriangleVector maResult; + + void handleClosingEdge(const B2DPoint& rStart, const B2DPoint& rEnd); + bool CheckPointInTriangle(EdgeEntry* pEdgeA, EdgeEntry const * pEdgeB, const B2DPoint& rTestPoint); + void createTriangle(const B2DPoint& rA, const B2DPoint& rB, const B2DPoint& rC); + + public: + explicit Triangulator(const B2DPolyPolygon& rCandidate); + + const triangulator::B2DTriangleVector& getResult() const { return maResult; } + }; + + void Triangulator::handleClosingEdge(const B2DPoint& rStart, const B2DPoint& rEnd) + { + // create an entry, else the comparison might use the wrong edges + EdgeEntry aNew(rStart, rEnd); + EdgeEntry* pCurr = mpList; + EdgeEntry* pPrev = nullptr; + + while(pCurr + && pCurr->getStart().getY() <= aNew.getStart().getY() + && *pCurr != aNew) + { + pPrev = pCurr; + pCurr = pCurr->getNext(); + } + + if(pCurr && *pCurr == aNew) + { + // found closing edge, remove + if(pPrev) + { + pPrev->setNext(pCurr->getNext()); + } + else + { + mpList = pCurr->getNext(); + } + } + else + { + // insert closing edge + EdgeEntry* pNew = new EdgeEntry(aNew); + maNewEdgeEntries.emplace_back(pNew); + pCurr = mpList; + pPrev = nullptr; + + while(pCurr && *pCurr < *pNew) + { + pPrev = pCurr; + pCurr = pCurr->getNext(); + } + + if(pPrev) + { + pNew->setNext(pPrev->getNext()); + pPrev->setNext(pNew); + } + else + { + pNew->setNext(mpList); + mpList = pNew; + } + } + } + + bool Triangulator::CheckPointInTriangle(EdgeEntry* pEdgeA, EdgeEntry const * pEdgeB, const B2DPoint& rTestPoint) + { + // inside triangle or on edge? + if(!utils::isPointInTriangle(pEdgeA->getStart(), pEdgeA->getEnd(), pEdgeB->getEnd(), rTestPoint, true)) + return true; + + // but not on point + if(!rTestPoint.equal(pEdgeA->getEnd()) && !rTestPoint.equal(pEdgeB->getEnd())) + { + // found point in triangle -> split triangle inserting two edges + EdgeEntry* pStart = new EdgeEntry(pEdgeA->getStart(), rTestPoint); + EdgeEntry* pEnd = new EdgeEntry(*pStart); + maNewEdgeEntries.emplace_back(pStart); + maNewEdgeEntries.emplace_back(pEnd); + + pStart->setNext(pEnd); + pEnd->setNext(pEdgeA->getNext()); + pEdgeA->setNext(pStart); + + return false; + } + + return true; + } + + void Triangulator::createTriangle(const B2DPoint& rA, const B2DPoint& rB, const B2DPoint& rC) + { + maResult.emplace_back( + rA, + rB, + rC); + } + + // consume as long as there are edges + Triangulator::Triangulator(const B2DPolyPolygon& rCandidate) + : mpList(nullptr) + { + // add all available edges to the single linked local list which will be sorted + // by Y,X,atan2 when adding nodes + if(rCandidate.count()) + { + for(const auto& rPolygonCandidate : rCandidate) + { + const sal_uInt32 nCount {rPolygonCandidate.count()}; + + if(nCount > 2) + { + B2DPoint aPrevPnt(rPolygonCandidate.getB2DPoint(nCount - 1)); + + for(sal_uInt32 b(0); b < nCount; b++) + { + B2DPoint aNextPnt(rPolygonCandidate.getB2DPoint(b)); + + if( !aPrevPnt.equal(aNextPnt) ) + { + maStartEntries.emplace_back(aPrevPnt, aNextPnt); + } + + aPrevPnt = aNextPnt; + } + } + } + + if(!maStartEntries.empty()) + { + // sort initial list + std::sort(maStartEntries.begin(), maStartEntries.end()); + + // insert to own simply linked list + EdgeEntries::iterator aPos(maStartEntries.begin()); + mpList = &(*aPos++); + EdgeEntry* pLast = mpList; + + while(aPos != maStartEntries.end()) + { + EdgeEntry* pEntry = &(*aPos++); + pLast->setNext(pEntry); + pLast = pEntry; + } + } + } + + while(mpList) + { + if(mpList->getNext() && mpList->getNext()->getStart().equal(mpList->getStart())) + { + // next candidate. There are two edges and start point is equal. + // Length is not zero. + EdgeEntry* pEdgeA = mpList; + EdgeEntry* pEdgeB = pEdgeA->getNext(); + + if( pEdgeA->getEnd().equal(pEdgeB->getEnd()) ) + { + // start and end equal -> neutral triangle, delete both + mpList = pEdgeB->getNext(); + } + else + { + const B2DVector aLeft(pEdgeA->getEnd() - pEdgeA->getStart()); + const B2DVector aRight(pEdgeB->getEnd() - pEdgeA->getStart()); + + if(getOrientation(aLeft, aRight) == B2VectorOrientation::Neutral) + { + // edges are parallel and have different length -> neutral triangle, + // delete both edges and handle closing edge + mpList = pEdgeB->getNext(); + handleClosingEdge(pEdgeA->getEnd(), pEdgeB->getEnd()); + } + else + { + // not parallel, look for points inside + B2DRange aRange(pEdgeA->getStart(), pEdgeA->getEnd()); + aRange.expand(pEdgeB->getEnd()); + EdgeEntry* pTestEdge = pEdgeB->getNext(); + bool bNoPointInTriangle(true); + + // look for start point in triangle + while(bNoPointInTriangle && pTestEdge) + { + if(aRange.getMaxY() < pTestEdge->getStart().getY()) + { + // edge is below test range and edges are sorted -> stop looking + break; + } + else + { + // do not look for edges with same start point, they are sorted and cannot end inside. + if(!pTestEdge->getStart().equal(pEdgeA->getStart())) + { + if(aRange.isInside(pTestEdge->getStart())) + { + bNoPointInTriangle = CheckPointInTriangle(pEdgeA, pEdgeB, pTestEdge->getStart()); + } + } + } + + // next candidate + pTestEdge = pTestEdge->getNext(); + } + + if(bNoPointInTriangle) + { + // look for end point in triangle + pTestEdge = pEdgeB->getNext(); + + while(bNoPointInTriangle && pTestEdge) + { + if(aRange.getMaxY() < pTestEdge->getStart().getY()) + { + // edge is below test range and edges are sorted -> stop looking + break; + } + else + { + // do not look for edges with same end point, they are sorted and cannot end inside. + if(!pTestEdge->getEnd().equal(pEdgeA->getStart())) + { + if(aRange.isInside(pTestEdge->getEnd())) + { + bNoPointInTriangle = CheckPointInTriangle(pEdgeA, pEdgeB, pTestEdge->getEnd()); + } + } + } + + // next candidate + pTestEdge = pTestEdge->getNext(); + } + } + + if(bNoPointInTriangle) + { + // create triangle, remove edges, handle closing edge + mpList = pEdgeB->getNext(); + createTriangle(pEdgeA->getStart(), pEdgeB->getEnd(), pEdgeA->getEnd()); + handleClosingEdge(pEdgeA->getEnd(), pEdgeB->getEnd()); + } + } + } + } + else + { + // only one entry at start point, delete it + mpList = mpList->getNext(); + } + } + } + + } // end of anonymous namespace +} // end of namespace basegfx + +namespace basegfx::triangulator +{ + B2DTriangleVector triangulate(const B2DPolygon& rCandidate) + { + B2DTriangleVector aRetval; + + // subdivide locally (triangulate does not work with beziers), remove double and neutral points + B2DPolygon aCandidate(rCandidate.areControlPointsUsed() ? utils::adaptiveSubdivideByAngle(rCandidate) : rCandidate); + aCandidate.removeDoublePoints(); + aCandidate = utils::removeNeutralPoints(aCandidate); + + if(aCandidate.count() == 2) + { + // candidate IS a triangle, just append + aRetval.emplace_back( + aCandidate.getB2DPoint(0), + aCandidate.getB2DPoint(1), + aCandidate.getB2DPoint(2)); + } + else if(aCandidate.count() > 2) + { + if(utils::isConvex(aCandidate)) + { + // polygon is convex, just use a triangle fan + utils::addTriangleFan(aCandidate, aRetval); + } + else + { + // polygon is concave. + const B2DPolyPolygon aCandPolyPoly(aCandidate); + Triangulator aTriangulator(aCandPolyPoly); + + aRetval = aTriangulator.getResult(); + } + } + + return aRetval; + } + + B2DTriangleVector triangulate(const B2DPolyPolygon& rCandidate) + { + B2DTriangleVector aRetval; + + // subdivide locally (triangulate does not work with beziers) + B2DPolyPolygon aCandidate(rCandidate.areControlPointsUsed() ? utils::adaptiveSubdivideByAngle(rCandidate) : rCandidate); + + if(aCandidate.count() == 1) + { + // single polygon -> single polygon triangulation + const B2DPolygon& aSinglePolygon(aCandidate.getB2DPolygon(0)); + + aRetval = triangulate(aSinglePolygon); + } + else + { + Triangulator aTriangulator(aCandidate); + + aRetval = aTriangulator.getResult(); + } + + return aRetval; + } +} // end of namespace + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/basegfx/source/polygon/b2dpolypolygon.cxx b/basegfx/source/polygon/b2dpolypolygon.cxx new file mode 100644 index 0000000000..547634dc4d --- /dev/null +++ b/basegfx/source/polygon/b2dpolypolygon.cxx @@ -0,0 +1,432 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * 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 . + */ + +#include <sal/config.h> + +#include <cassert> +#include <utility> + +#include <basegfx/polygon/b2dpolypolygon.hxx> +#include <basegfx/polygon/b2dpolygon.hxx> +#include <basegfx/matrix/b2dhommatrix.hxx> +#include <basegfx/utils/systemdependentdata.hxx> + +namespace basegfx +{ + +class ImplB2DPolyPolygon +{ + basegfx::B2DPolygonVector maPolygons; + // we do not want to 'modify' the ImplB2DPolyPolygon, + // but add buffered data that is valid for all referencing instances + mutable std::unique_ptr<basegfx::SystemDependentDataHolder> mpSystemDependentDataHolder; + +public: + ImplB2DPolyPolygon() + { + } + + explicit ImplB2DPolyPolygon(const ImplB2DPolyPolygon& rSource) + : maPolygons(rSource.maPolygons) + { + } + + explicit ImplB2DPolyPolygon(const basegfx::B2DPolygon& rToBeCopied) + : maPolygons(1,rToBeCopied) + { + } + + ImplB2DPolyPolygon& operator=(const ImplB2DPolyPolygon& rSource) + { + if (this != &rSource) + { + maPolygons = rSource.maPolygons; + mpSystemDependentDataHolder.reset(); + } + + return *this; + } + + void addOrReplaceSystemDependentData(basegfx::SystemDependentData_SharedPtr& rData) const + { + if(!mpSystemDependentDataHolder) + { + mpSystemDependentDataHolder.reset(new basegfx::SystemDependentDataHolder()); + } + + mpSystemDependentDataHolder->addOrReplaceSystemDependentData(rData); + } + + basegfx::SystemDependentData_SharedPtr getSystemDependentData(size_t hash_code) const + { + if(!mpSystemDependentDataHolder) + { + return basegfx::SystemDependentData_SharedPtr(); + } + + return mpSystemDependentDataHolder->getSystemDependentData(hash_code); + } + + bool operator==(const ImplB2DPolyPolygon& rPolygonList) const + { + return maPolygons == rPolygonList.maPolygons; + } + + const basegfx::B2DPolygon& getB2DPolygon(sal_uInt32 nIndex) const + { + assert(nIndex < maPolygons.size()); + return maPolygons[nIndex]; + } + + void setB2DPolygon(sal_uInt32 nIndex, const basegfx::B2DPolygon& rPolygon) + { + assert(nIndex < maPolygons.size()); + maPolygons[nIndex] = rPolygon; + } + + void insert(sal_uInt32 nIndex, const basegfx::B2DPolygon& rPolygon, sal_uInt32 nCount) + { + assert(nCount > 0); + assert(nIndex <= maPolygons.size()); + // add nCount copies of rPolygon + maPolygons.insert(maPolygons.begin() + nIndex, nCount, rPolygon); + } + + void append(const basegfx::B2DPolygon& rPolygon, sal_uInt32 nCount) + { + insert(maPolygons.size(), rPolygon, nCount); + } + + void reserve(sal_uInt32 nCount) + { + maPolygons.reserve(nCount); + } + + void insert(sal_uInt32 nIndex, const basegfx::B2DPolyPolygon& rPolyPolygon) + { + assert(nIndex <= maPolygons.size()); + // add nCount polygons from rPolyPolygon + maPolygons.insert(maPolygons.begin() + nIndex, rPolyPolygon.begin(), rPolyPolygon.end()); + } + + void append(const basegfx::B2DPolyPolygon& rPolyPolygon) + { + insert(maPolygons.size(), rPolyPolygon); + } + + void remove(sal_uInt32 nIndex, sal_uInt32 nCount) + { + assert(nCount > 0); + assert(nIndex + nCount <= maPolygons.size()); + // remove polygon data + auto aStart(maPolygons.begin() + nIndex); + auto aEnd(aStart + nCount); + + maPolygons.erase(aStart, aEnd); + } + + sal_uInt32 count() const + { + return maPolygons.size(); + } + + void setClosed(bool bNew) + { + for(basegfx::B2DPolygon & rPolygon : maPolygons) + { + rPolygon.setClosed(bNew); + } + } + + void flip() + { + for (auto& aPolygon : maPolygons) + aPolygon.flip(); + } + + void removeDoublePoints() + { + for (auto& aPolygon : maPolygons) + aPolygon.removeDoublePoints(); + } + + void transform(const basegfx::B2DHomMatrix& rMatrix) + { + for (auto& aPolygon : maPolygons) + aPolygon.transform(rMatrix); + } + + void makeUnique() + { + for (auto& aPolygon : maPolygons) + aPolygon.makeUnique(); + } + + const basegfx::B2DPolygon* begin() const + { + if (maPolygons.empty()) + return nullptr; + else + return maPolygons.data(); + } + + const basegfx::B2DPolygon* end() const + { + if (maPolygons.empty()) + return nullptr; + else + return maPolygons.data() + maPolygons.size(); + } + + basegfx::B2DPolygon* begin() + { + if (maPolygons.empty()) + return nullptr; + else + return maPolygons.data(); + } + + basegfx::B2DPolygon* end() + { + if (maPolygons.empty()) + return nullptr; + else + return maPolygons.data() + maPolygons.size(); + } +}; + + B2DPolyPolygon::B2DPolyPolygon() = default; + + B2DPolyPolygon::B2DPolyPolygon(const B2DPolyPolygon&) = default; + + B2DPolyPolygon::B2DPolyPolygon(B2DPolyPolygon&&) = default; + + B2DPolyPolygon::B2DPolyPolygon(const B2DPolygon& rPolygon) : + mpPolyPolygon( ImplB2DPolyPolygon(rPolygon) ) + { + } + + B2DPolyPolygon::~B2DPolyPolygon() = default; + + B2DPolyPolygon& B2DPolyPolygon::operator=(const B2DPolyPolygon&) = default; + + B2DPolyPolygon& B2DPolyPolygon::operator=(B2DPolyPolygon&&) = default; + + void B2DPolyPolygon::makeUnique() + { + mpPolyPolygon->makeUnique(); // non-const cow_wrapper::operator-> calls make_unique + } + + bool B2DPolyPolygon::operator==(const B2DPolyPolygon& rPolyPolygon) const + { + if(mpPolyPolygon.same_object(rPolyPolygon.mpPolyPolygon)) + return true; + + return ((*mpPolyPolygon) == (*rPolyPolygon.mpPolyPolygon)); + } + + bool B2DPolyPolygon::operator!=(const B2DPolyPolygon& rPolyPolygon) const + { + return !((*this) == rPolyPolygon); + } + + sal_uInt32 B2DPolyPolygon::count() const + { + return mpPolyPolygon->count(); + } + + B2DPolygon const & B2DPolyPolygon::getB2DPolygon(sal_uInt32 nIndex) const + { + return mpPolyPolygon->getB2DPolygon(nIndex); + } + + void B2DPolyPolygon::setB2DPolygon(sal_uInt32 nIndex, const B2DPolygon& rPolygon) + { + if(getB2DPolygon(nIndex) != rPolygon) + mpPolyPolygon->setB2DPolygon(nIndex, rPolygon); + } + + bool B2DPolyPolygon::areControlPointsUsed() const + { + for(sal_uInt32 a(0); a < count(); a++) + { + if(getB2DPolygon(a).areControlPointsUsed()) + { + return true; + } + } + + return false; + } + + void B2DPolyPolygon::insert(sal_uInt32 nIndex, const B2DPolygon& rPolygon, sal_uInt32 nCount) + { + if(nCount) + mpPolyPolygon->insert(nIndex, rPolygon, nCount); + } + + void B2DPolyPolygon::append(const B2DPolygon& rPolygon, sal_uInt32 nCount) + { + if(nCount) + mpPolyPolygon->append(rPolygon, nCount); + } + + void B2DPolyPolygon::reserve(sal_uInt32 nCount) + { + if(nCount) + mpPolyPolygon->reserve(nCount); + } + + B2DPolyPolygon B2DPolyPolygon::getDefaultAdaptiveSubdivision() const + { + B2DPolyPolygon aRetval; + if (count()) + { + ImplB2DPolyPolygon& dest = *aRetval.mpPolyPolygon; + dest.reserve(count()); + + for (sal_uInt32 a(0); a < count(); a++) + { + dest.append(getB2DPolygon(a).getDefaultAdaptiveSubdivision(), 1); + } + } + + return aRetval; + } + + B2DRange B2DPolyPolygon::getB2DRange() const + { + B2DRange aRetval; + + for(sal_uInt32 a(0); a < count(); a++) + { + aRetval.expand(getB2DPolygon(a).getB2DRange()); + } + + return aRetval; + } + + void B2DPolyPolygon::insert(sal_uInt32 nIndex, const B2DPolyPolygon& rPolyPolygon) + { + if(rPolyPolygon.count()) + mpPolyPolygon->insert(nIndex, rPolyPolygon); + } + + void B2DPolyPolygon::append(const B2DPolyPolygon& rPolyPolygon) + { + if(rPolyPolygon.count()) + mpPolyPolygon->append(rPolyPolygon); + } + + void B2DPolyPolygon::remove(sal_uInt32 nIndex, sal_uInt32 nCount) + { + if(nCount) + mpPolyPolygon->remove(nIndex, nCount); + } + + void B2DPolyPolygon::clear() + { + *mpPolyPolygon = ImplB2DPolyPolygon(); + } + + bool B2DPolyPolygon::isClosed() const + { + // PolyPOlygon is closed when all contained Polygons are closed or + // no Polygon exists. + for(sal_uInt32 a(0); a < count(); a++) + { + if(!getB2DPolygon(a).isClosed()) + return false; + } + + return true; + } + + void B2DPolyPolygon::setClosed(bool bNew) + { + if(bNew != isClosed()) + mpPolyPolygon->setClosed(bNew); + } + + void B2DPolyPolygon::flip() + { + if(count()) + { + mpPolyPolygon->flip(); + } + } + + bool B2DPolyPolygon::hasDoublePoints() const + { + for(sal_uInt32 a(0); a < count(); a++) + { + if(getB2DPolygon(a).hasDoublePoints()) + return true; + } + + return false; + } + + void B2DPolyPolygon::removeDoublePoints() + { + if(hasDoublePoints()) + mpPolyPolygon->removeDoublePoints(); + } + + void B2DPolyPolygon::transform(const B2DHomMatrix& rMatrix) + { + if(count() && !rMatrix.isIdentity()) + { + mpPolyPolygon->transform(rMatrix); + } + } + + const B2DPolygon* B2DPolyPolygon::begin() const + { + return mpPolyPolygon->begin(); + } + + const B2DPolygon* B2DPolyPolygon::end() const + { + return mpPolyPolygon->end(); + } + + B2DPolygon* B2DPolyPolygon::begin() + { + return mpPolyPolygon->begin(); + } + + B2DPolygon* B2DPolyPolygon::end() + { + return mpPolyPolygon->end(); + } + + void B2DPolyPolygon::addOrReplaceSystemDependentDataInternal(SystemDependentData_SharedPtr& rData) const + { + mpPolyPolygon->addOrReplaceSystemDependentData(rData); + } + + SystemDependentData_SharedPtr B2DPolyPolygon::getSystemDependantDataInternal(size_t hash_code) const + { + return mpPolyPolygon->getSystemDependentData(hash_code); + } + +} // end of namespace basegfx + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/basegfx/source/polygon/b2dpolypolygoncutter.cxx b/basegfx/source/polygon/b2dpolypolygoncutter.cxx new file mode 100644 index 0000000000..42cfed615f --- /dev/null +++ b/basegfx/source/polygon/b2dpolypolygoncutter.cxx @@ -0,0 +1,1135 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * 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 . + */ + +#include <basegfx/numeric/ftools.hxx> +#include <basegfx/polygon/b2dpolypolygoncutter.hxx> +#include <basegfx/point/b2dpoint.hxx> +#include <basegfx/vector/b2dvector.hxx> +#include <basegfx/polygon/b2dpolygon.hxx> +#include <basegfx/polygon/b2dpolygontools.hxx> +#include <basegfx/polygon/b2dpolygoncutandtouch.hxx> +#include <basegfx/range/b2drange.hxx> +#include <basegfx/polygon/b2dpolypolygontools.hxx> +#include <basegfx/curve/b2dcubicbezier.hxx> +#include <sal/log.hxx> +#include <utility> +#include <vector> +#include <algorithm> +#include <numeric> + +namespace basegfx +{ + namespace + { + + struct StripHelper + { + B2DRange maRange; + sal_Int32 mnDepth; + B2VectorOrientation meOrinetation; + }; + + struct PN + { + public: + B2DPoint maPoint; + sal_uInt32 mnI; + sal_uInt32 mnIP; + sal_uInt32 mnIN; + }; + + struct VN + { + public: + B2DVector maPrev; + B2DVector maNext; + + // to have the correct curve segments in the crossover checks, + // it is necessary to keep the original next vectors, too. Else, + // it may happen to use an already switched next vector which + // would interpolate the wrong comparison point + B2DVector maOriginalNext; + }; + + struct SN + { + public: + PN* mpPN; + + bool operator<(const SN& rComp) const + { + if(fTools::equal(mpPN->maPoint.getX(), rComp.mpPN->maPoint.getX())) + { + if(fTools::equal(mpPN->maPoint.getY(), rComp.mpPN->maPoint.getY())) + { + return (mpPN->mnI < rComp.mpPN->mnI); + } + else + { + return fTools::less(mpPN->maPoint.getY(), rComp.mpPN->maPoint.getY()); + } + } + else + { + return fTools::less(mpPN->maPoint.getX(), rComp.mpPN->maPoint.getX()); + } + } + }; + + typedef std::vector< PN > PNV; + typedef std::vector< VN > VNV; + typedef std::vector< SN > SNV; + typedef std::pair< basegfx::B2DPoint /*orig*/, basegfx::B2DPoint /*repl*/ > CorrectionPair; + + class solver + { + private: + const B2DPolyPolygon maOriginal; + PNV maPNV; + VNV maVNV; + SNV maSNV; + std::vector< CorrectionPair > + maCorrectionTable; + + bool mbIsCurve : 1; + bool mbChanged : 1; + + void impAddPolygon(const sal_uInt32 aPos, const B2DPolygon& rGeometry) + { + const sal_uInt32 nCount(rGeometry.count()); + PN aNewPN; + VN aNewVN; + SN aNewSN; + + for(sal_uInt32 a(0); a < nCount; a++) + { + const B2DPoint aPoint(rGeometry.getB2DPoint(a)); + aNewPN.maPoint = aPoint; + aNewPN.mnI = aPos + a; + aNewPN.mnIP = aPos + ((a != 0) ? a - 1 : nCount - 1); + aNewPN.mnIN = aPos + ((a + 1 == nCount) ? 0 : a + 1); + maPNV.push_back(aNewPN); + + if(mbIsCurve) + { + aNewVN.maPrev = rGeometry.getPrevControlPoint(a) - aPoint; + aNewVN.maNext = rGeometry.getNextControlPoint(a) - aPoint; + aNewVN.maOriginalNext = aNewVN.maNext; + maVNV.push_back(aNewVN); + } + + aNewSN.mpPN = &maPNV[maPNV.size() - 1]; + maSNV.push_back(aNewSN); + } + } + + static bool impLeftOfEdges(const B2DVector& rVecA, const B2DVector& rVecB, const B2DVector& rTest) + { + // tests if rTest is left of both directed line segments along the line -rVecA, rVecB. Test is + // with border. + if(rVecA.cross(rVecB) > 0.0) + { + // b is left turn seen from a, test if Test is left of both and so inside (left is seen as inside) + const bool bBoolA(fTools::moreOrEqual(rVecA.cross(rTest), 0.0)); + const bool bBoolB(fTools::lessOrEqual(rVecB.cross(rTest), 0.0)); + + return (bBoolA && bBoolB); + } + else + { + // b is right turn seen from a, test if Test is right of both and so outside (left is seen as inside) + const bool bBoolA(fTools::lessOrEqual(rVecA.cross(rTest), 0.0)); + const bool bBoolB(fTools::moreOrEqual(rVecB.cross(rTest), 0.0)); + + return (!(bBoolA && bBoolB)); + } + } + + void impSwitchNext(PN& rPNa, PN& rPNb) + { + std::swap(rPNa.mnIN, rPNb.mnIN); + + if(mbIsCurve) + { + VN& rVNa = maVNV[rPNa.mnI]; + VN& rVNb = maVNV[rPNb.mnI]; + + std::swap(rVNa.maNext, rVNb.maNext); + } + + if(!mbChanged) + { + mbChanged = true; + } + } + + B2DCubicBezier createSegment(const PN& rPN, bool bPrev) const + { + const B2DPoint& rStart(rPN.maPoint); + const B2DPoint& rEnd(maPNV[bPrev ? rPN.mnIP : rPN.mnIN].maPoint); + const B2DVector& rCPA(bPrev ? maVNV[rPN.mnI].maPrev : maVNV[rPN.mnI].maNext); + // Use maOriginalNext, not maNext to create the original (yet unchanged) + // curve segment. Otherwise, this segment would NOT ne correct. + const B2DVector& rCPB(bPrev ? maVNV[maPNV[rPN.mnIP].mnI].maOriginalNext : maVNV[maPNV[rPN.mnIN].mnI].maPrev); + + return B2DCubicBezier(rStart, rStart + rCPA, rEnd + rCPB, rEnd); + } + + void impHandleCommon(PN& rPNa, PN& rPNb) + { + if(mbIsCurve) + { + const B2DCubicBezier aNextA(createSegment(rPNa, false)); + const B2DCubicBezier aPrevA(createSegment(rPNa, true)); + + if(aNextA.equal(aPrevA)) + { + // deadend on A (identical edge) + return; + } + + const B2DCubicBezier aNextB(createSegment(rPNb, false)); + const B2DCubicBezier aPrevB(createSegment(rPNb, true)); + + if(aNextB.equal(aPrevB)) + { + // deadend on B (identical edge) + return; + } + + if(aPrevA.equal(aPrevB)) + { + // common edge in same direction + return; + } + else if(aPrevA.equal(aNextB)) + { + // common edge in opposite direction + if(aNextA.equal(aPrevB)) + { + // common edge in opposite direction continues + return; + } + else + { + // common edge in opposite direction leave + impSwitchNext(rPNa, rPNb); + } + } + else if(aNextA.equal(aNextB)) + { + // common edge in same direction enter + // search leave edge + PN* pPNa2 = &maPNV[rPNa.mnIN]; + PN* pPNb2 = &maPNV[rPNb.mnIN]; + bool bOnEdge(true); + + do + { + const B2DCubicBezier aNextA2(createSegment(*pPNa2, false)); + const B2DCubicBezier aNextB2(createSegment(*pPNb2, false)); + + if(aNextA2.equal(aNextB2)) + { + pPNa2 = &maPNV[pPNa2->mnIN]; + pPNb2 = &maPNV[pPNb2->mnIN]; + } + else + { + bOnEdge = false; + } + } + while(bOnEdge && pPNa2 != &rPNa && pPNb2 != &rPNb); + + if(bOnEdge) + { + // loop over two identical polygon paths + return; + } + else + { + // enter at rPNa, rPNb; leave at pPNa2, pPNb2. No common edges + // at enter/leave. Check for crossover. + const B2DVector aPrevCA(aPrevA.interpolatePoint(0.5) - aPrevA.getStartPoint()); + const B2DVector aNextCA(aNextA.interpolatePoint(0.5) - aNextA.getStartPoint()); + const B2DVector aPrevCB(aPrevB.interpolatePoint(0.5) - aPrevB.getStartPoint()); + const bool bEnter(impLeftOfEdges(aPrevCA, aNextCA, aPrevCB)); + + const B2DCubicBezier aNextA2(createSegment(*pPNa2, false)); + const B2DCubicBezier aPrevA2(createSegment(*pPNa2, true)); + const B2DCubicBezier aNextB2(createSegment(*pPNb2, false)); + const B2DVector aPrevCA2(aPrevA2.interpolatePoint(0.5) - aPrevA2.getStartPoint()); + const B2DVector aNextCA2(aNextA2.interpolatePoint(0.5) - aNextA2.getStartPoint()); + const B2DVector aNextCB2(aNextB2.interpolatePoint(0.5) - aNextB2.getStartPoint()); + const bool bLeave(impLeftOfEdges(aPrevCA2, aNextCA2, aNextCB2)); + + if(bEnter != bLeave) + { + // crossover + impSwitchNext(rPNa, rPNb); + } + } + } + else if(aNextA.equal(aPrevB)) + { + // common edge in opposite direction enter + impSwitchNext(rPNa, rPNb); + } + else + { + // no common edges, check for crossover + const B2DVector aPrevCA(aPrevA.interpolatePoint(0.5) - aPrevA.getStartPoint()); + const B2DVector aNextCA(aNextA.interpolatePoint(0.5) - aNextA.getStartPoint()); + const B2DVector aPrevCB(aPrevB.interpolatePoint(0.5) - aPrevB.getStartPoint()); + const B2DVector aNextCB(aNextB.interpolatePoint(0.5) - aNextB.getStartPoint()); + + const bool bEnter(impLeftOfEdges(aPrevCA, aNextCA, aPrevCB)); + const bool bLeave(impLeftOfEdges(aPrevCA, aNextCA, aNextCB)); + + if(bEnter != bLeave) + { + // crossover + impSwitchNext(rPNa, rPNb); + } + } + } + else + { + const B2DPoint& rNextA(maPNV[rPNa.mnIN].maPoint); + const B2DPoint& rPrevA(maPNV[rPNa.mnIP].maPoint); + + if(rNextA.equal(rPrevA)) + { + // deadend on A + return; + } + + const B2DPoint& rNextB(maPNV[rPNb.mnIN].maPoint); + const B2DPoint& rPrevB(maPNV[rPNb.mnIP].maPoint); + + if(rNextB.equal(rPrevB)) + { + // deadend on B + return; + } + + if(rPrevA.equal(rPrevB)) + { + // common edge in same direction + return; + } + else if(rPrevA.equal(rNextB)) + { + // common edge in opposite direction + if(rNextA.equal(rPrevB)) + { + // common edge in opposite direction continues + return; + } + else + { + // common edge in opposite direction leave + impSwitchNext(rPNa, rPNb); + } + } + else if(rNextA.equal(rNextB)) + { + // common edge in same direction enter + // search leave edge + PN* pPNa2 = &maPNV[rPNa.mnIN]; + PN* pPNb2 = &maPNV[rPNb.mnIN]; + bool bOnEdge(true); + + do + { + const B2DPoint& rNextA2(maPNV[pPNa2->mnIN].maPoint); + const B2DPoint& rNextB2(maPNV[pPNb2->mnIN].maPoint); + + if(rNextA2.equal(rNextB2)) + { + pPNa2 = &maPNV[pPNa2->mnIN]; + pPNb2 = &maPNV[pPNb2->mnIN]; + } + else + { + bOnEdge = false; + } + } + while(bOnEdge && pPNa2 != &rPNa && pPNb2 != &rPNb); + + if(bOnEdge) + { + // loop over two identical polygon paths + return; + } + else + { + // enter at rPNa, rPNb; leave at pPNa2, pPNb2. No common edges + // at enter/leave. Check for crossover. + const B2DPoint& aPointE(rPNa.maPoint); + const B2DVector aPrevAE(rPrevA - aPointE); + const B2DVector aNextAE(rNextA - aPointE); + const B2DVector aPrevBE(rPrevB - aPointE); + + const B2DPoint& aPointL(pPNa2->maPoint); + const B2DVector aPrevAL(maPNV[pPNa2->mnIP].maPoint - aPointL); + const B2DVector aNextAL(maPNV[pPNa2->mnIN].maPoint - aPointL); + const B2DVector aNextBL(maPNV[pPNb2->mnIN].maPoint - aPointL); + + const bool bEnter(impLeftOfEdges(aPrevAE, aNextAE, aPrevBE)); + const bool bLeave(impLeftOfEdges(aPrevAL, aNextAL, aNextBL)); + + if(bEnter != bLeave) + { + // crossover; switch start or end + impSwitchNext(rPNa, rPNb); + } + } + } + else if(rNextA.equal(rPrevB)) + { + // common edge in opposite direction enter + impSwitchNext(rPNa, rPNb); + } + else + { + // no common edges, check for crossover + const B2DPoint& aPoint(rPNa.maPoint); + const B2DVector aPrevA(rPrevA - aPoint); + const B2DVector aNextA(rNextA - aPoint); + const B2DVector aPrevB(rPrevB - aPoint); + const B2DVector aNextB(rNextB - aPoint); + + const bool bEnter(impLeftOfEdges(aPrevA, aNextA, aPrevB)); + const bool bLeave(impLeftOfEdges(aPrevA, aNextA, aNextB)); + + if(bEnter != bLeave) + { + // crossover + impSwitchNext(rPNa, rPNb); + } + } + } + } + + void impSolve() + { + // sort by point to identify common nodes easier + std::sort(maSNV.begin(), maSNV.end()); + + // handle common nodes + const sal_uInt32 nNodeCount(maSNV.size()); + sal_uInt32 a(0); + + // snap unsharp-equal points + if(nNodeCount) + { + basegfx::B2DPoint* pLast(&maSNV[0].mpPN->maPoint); + + for(const auto& rSN : maSNV) + { + basegfx::B2DPoint* pCurrent(&rSN.mpPN->maPoint); + + if(pLast->equal(*pCurrent) && (pLast->getX() != pCurrent->getX() || pLast->getY() != pCurrent->getY())) + { + const basegfx::B2DPoint aMiddle((*pLast + *pCurrent) * 0.5); + + if(pLast->getX() != aMiddle.getX() || pLast->getY() != aMiddle.getY()) + { + maCorrectionTable.emplace_back(*pLast, aMiddle); + *pLast = aMiddle; + } + + if(pCurrent->getX() != aMiddle.getX() || pCurrent->getY() != aMiddle.getY()) + { + maCorrectionTable.emplace_back(*pCurrent, aMiddle); + *pCurrent = aMiddle; + } + } + + pLast = pCurrent; + } + } + + for(a = 0; a < nNodeCount - 1; a++) + { + // test a before using it, not after. Also use nPointCount instead of aSortNodes.size() + PN& rPNb = *(maSNV[a].mpPN); + + for(sal_uInt32 b(a + 1); b < nNodeCount && rPNb.maPoint.equal(maSNV[b].mpPN->maPoint); b++) + { + impHandleCommon(rPNb, *maSNV[b].mpPN); + } + } + } + + public: + explicit solver(const B2DPolygon& rOriginal) + : maOriginal(B2DPolyPolygon(rOriginal)), + mbIsCurve(false), + mbChanged(false) + { + const sal_uInt32 nOriginalCount(rOriginal.count()); + + if(!nOriginalCount) + return; + + B2DPolygon aGeometry(utils::addPointsAtCutsAndTouches(rOriginal)); + aGeometry.removeDoublePoints(); + aGeometry = utils::simplifyCurveSegments(aGeometry); + mbIsCurve = aGeometry.areControlPointsUsed(); + + const sal_uInt32 nPointCount(aGeometry.count()); + + // If it's not a bezier polygon, at least four points are needed to create + // a self-intersection. If it's a bezier polygon, the minimum point number + // is two, since with a single point You get a curve, but no self-intersection + if(!(nPointCount > 3 || (nPointCount > 1 && mbIsCurve))) + return; + + // reserve space in point, control and sort vector. + maSNV.reserve(nPointCount); + maPNV.reserve(nPointCount); + maVNV.reserve(mbIsCurve ? nPointCount : 0); + + // fill data + impAddPolygon(0, aGeometry); + + // solve common nodes + impSolve(); + } + + explicit solver(B2DPolyPolygon aOriginal, size_t* pPointLimit = nullptr) + : maOriginal(std::move(aOriginal)), + mbIsCurve(false), + mbChanged(false) + { + sal_uInt32 nOriginalCount(maOriginal.count()); + + if(!nOriginalCount) + return; + + B2DPolyPolygon aGeometry(utils::addPointsAtCutsAndTouches(maOriginal, pPointLimit)); + aGeometry.removeDoublePoints(); + aGeometry = utils::simplifyCurveSegments(aGeometry); + mbIsCurve = aGeometry.areControlPointsUsed(); + nOriginalCount = aGeometry.count(); + + if(!nOriginalCount) + return; + + // If it's not a bezier curve, at least three points would be needed to have a + // topological relevant (not empty) polygon. Since it's not known here if trivial + // edges (dead ends) will be kept or sorted out, add non-bezier polygons with + // more than one point. + // For bezier curves, the minimum for defining an area is also one. + sal_uInt32 nPointCount = std::accumulate( aGeometry.begin(), aGeometry.end(), sal_uInt32(0), + [](sal_uInt32 a, const basegfx::B2DPolygon& b){return a + b.count();}); + + if(!nPointCount) + return; + + // reserve space in point, control and sort vector. + maSNV.reserve(nPointCount); + maPNV.reserve(nPointCount); + maVNV.reserve(mbIsCurve ? nPointCount : 0); + + // fill data + sal_uInt32 nInsertIndex(0); + + for(const auto& rCandidate : aGeometry ) + { + const sal_uInt32 nCandCount(rCandidate.count()); + + // use same condition as above, the data vector is + // pre-allocated + if(nCandCount) + { + impAddPolygon(nInsertIndex, rCandidate); + nInsertIndex += nCandCount; + } + } + + // solve common nodes + impSolve(); + } + + B2DPolyPolygon getB2DPolyPolygon() + { + if(mbChanged) + { + B2DPolyPolygon aRetval; + const sal_uInt32 nCount(maPNV.size()); + sal_uInt32 nCountdown(nCount); + + for(sal_uInt32 a(0); nCountdown && a < nCount; a++) + { + PN& rPN = maPNV[a]; + + if(rPN.mnI != SAL_MAX_UINT32) + { + // unused node, start new part polygon + B2DPolygon aNewPart; + PN* pPNCurr = &rPN; + + do + { + const B2DPoint& rPoint = pPNCurr->maPoint; + aNewPart.append(rPoint); + + if(mbIsCurve) + { + const VN& rVNCurr = maVNV[pPNCurr->mnI]; + + if(!rVNCurr.maPrev.equalZero()) + { + aNewPart.setPrevControlPoint(aNewPart.count() - 1, rPoint + rVNCurr.maPrev); + } + + if(!rVNCurr.maNext.equalZero()) + { + aNewPart.setNextControlPoint(aNewPart.count() - 1, rPoint + rVNCurr.maNext); + } + } + + pPNCurr->mnI = SAL_MAX_UINT32; + nCountdown--; + pPNCurr = &(maPNV[pPNCurr->mnIN]); + } + while(pPNCurr != &rPN && pPNCurr->mnI != SAL_MAX_UINT32); + + // close and add + aNewPart.setClosed(true); + aRetval.append(aNewPart); + } + } + + return aRetval; + } + else + { + const sal_uInt32 nCorrectionSize(maCorrectionTable.size()); + + // no change, return original + if(!nCorrectionSize) + { + return maOriginal; + } + + // apply coordinate corrections to ensure inside/outside correctness after solving + const sal_uInt32 nPolygonCount(maOriginal.count()); + basegfx::B2DPolyPolygon aRetval(maOriginal); + + for(sal_uInt32 a(0); a < nPolygonCount; a++) + { + basegfx::B2DPolygon aTemp(aRetval.getB2DPolygon(a)); + const sal_uInt32 nPointCount(aTemp.count()); + bool bChanged(false); + + for(sal_uInt32 b(0); b < nPointCount; b++) + { + const basegfx::B2DPoint aCandidate(aTemp.getB2DPoint(b)); + + for(sal_uInt32 c(0); c < nCorrectionSize; c++) + { + if(maCorrectionTable[c].first.getX() == aCandidate.getX() && maCorrectionTable[c].first.getY() == aCandidate.getY()) + { + aTemp.setB2DPoint(b, maCorrectionTable[c].second); + bChanged = true; + } + } + } + + if(bChanged) + { + aRetval.setB2DPolygon(a, aTemp); + } + } + + return aRetval; + } + } + }; + + } // end of anonymous namespace +} // end of namespace basegfx + +namespace basegfx::utils +{ + + B2DPolyPolygon solveCrossovers(const B2DPolyPolygon& rCandidate, size_t* pPointLimit) + { + if(rCandidate.count() > 0) + { + solver aSolver(rCandidate, pPointLimit); + return aSolver.getB2DPolyPolygon(); + } + else + { + return rCandidate; + } + } + + B2DPolyPolygon solveCrossovers(const B2DPolygon& rCandidate) + { + solver aSolver(rCandidate); + return aSolver.getB2DPolyPolygon(); + } + + B2DPolyPolygon stripNeutralPolygons(const B2DPolyPolygon& rCandidate) + { + B2DPolyPolygon aRetval; + + for(const auto& rPolygon : rCandidate) + { + if(utils::getOrientation(rPolygon) != B2VectorOrientation::Neutral) + { + aRetval.append(rPolygon); + } + } + + return aRetval; + } + + B2DPolyPolygon createNonzeroConform(const B2DPolyPolygon& rCandidate) + { + if (rCandidate.count() > 1000) + { + SAL_WARN("basegfx", "this poly is too large, " << rCandidate.count() << " elements, to be able to process timeously, falling back to ignoring the winding rule, which is likely to cause rendering artifacts"); + return rCandidate; + } + + B2DPolyPolygon aCandidate; + + // remove all self-intersections and intersections + if(rCandidate.count() == 1) + { + aCandidate = basegfx::utils::solveCrossovers(rCandidate.getB2DPolygon(0)); + } + else + { + aCandidate = basegfx::utils::solveCrossovers(rCandidate); + } + + // cleanup evtl. neutral polygons + aCandidate = basegfx::utils::stripNeutralPolygons(aCandidate); + + // remove all polygons which have the same orientation as the polygon they are directly contained in + const sal_uInt32 nCount(aCandidate.count()); + + if(nCount > 1) + { + sal_uInt32 a, b; + std::vector< StripHelper > aHelpers; + aHelpers.resize(nCount); + + for(a = 0; a < nCount; a++) + { + const B2DPolygon& aCand(aCandidate.getB2DPolygon(a)); + StripHelper* pNewHelper = &(aHelpers[a]); + pNewHelper->maRange = utils::getRange(aCand); + pNewHelper->meOrinetation = utils::getOrientation(aCand); + + // initialize with own orientation + pNewHelper->mnDepth = (pNewHelper->meOrinetation == B2VectorOrientation::Negative ? -1 : 1); + } + + for(a = 0; a < nCount - 1; a++) + { + const B2DPolygon& aCandA(aCandidate.getB2DPolygon(a)); + StripHelper& rHelperA = aHelpers[a]; + + for(b = a + 1; b < nCount; b++) + { + const B2DPolygon& aCandB(aCandidate.getB2DPolygon(b)); + StripHelper& rHelperB = aHelpers[b]; + const bool bAInB(rHelperB.maRange.isInside(rHelperA.maRange) && utils::isInside(aCandB, aCandA, true)); + + if(bAInB) + { + // A is inside B, add orientation of B to A + rHelperA.mnDepth += (rHelperB.meOrinetation == B2VectorOrientation::Negative ? -1 : 1); + } + + const bool bBInA(rHelperA.maRange.isInside(rHelperB.maRange) && utils::isInside(aCandA, aCandB, true)); + + if(bBInA) + { + // B is inside A, add orientation of A to B + rHelperB.mnDepth += (rHelperA.meOrinetation == B2VectorOrientation::Negative ? -1 : 1); + } + } + } + + const B2DPolyPolygon aSource(aCandidate); + aCandidate.clear(); + + for(a = 0; a < nCount; a++) + { + const StripHelper& rHelper = aHelpers[a]; + // for contained unequal oriented polygons sum will be 0 + // for contained equal it will be >=2 or <=-2 + // for free polygons (not contained) it will be 1 or -1 + // -> accept all which are >=-1 && <= 1 + bool bAcceptEntry(rHelper.mnDepth >= -1 && rHelper.mnDepth <= 1); + + if(bAcceptEntry) + { + aCandidate.append(aSource.getB2DPolygon(a)); + } + } + } + + return aCandidate; + } + + B2DPolyPolygon stripDispensablePolygons(const B2DPolyPolygon& rCandidate, bool bKeepAboveZero) + { + const sal_uInt32 nCount(rCandidate.count()); + B2DPolyPolygon aRetval; + + if(nCount) + { + if(nCount == 1) + { + if(!bKeepAboveZero && utils::getOrientation(rCandidate.getB2DPolygon(0)) == B2VectorOrientation::Positive) + { + aRetval = rCandidate; + } + } + else + { + sal_uInt32 a, b; + std::vector< StripHelper > aHelpers; + aHelpers.resize(nCount); + + for(a = 0; a < nCount; a++) + { + const B2DPolygon& aCandidate(rCandidate.getB2DPolygon(a)); + StripHelper* pNewHelper = &(aHelpers[a]); + pNewHelper->maRange = utils::getRange(aCandidate); + pNewHelper->meOrinetation = utils::getOrientation(aCandidate); + pNewHelper->mnDepth = (pNewHelper->meOrinetation == B2VectorOrientation::Negative ? -1 : 0); + } + + for(a = 0; a < nCount - 1; a++) + { + const B2DPolygon& aCandA(rCandidate.getB2DPolygon(a)); + StripHelper& rHelperA = aHelpers[a]; + + for(b = a + 1; b < nCount; b++) + { + const B2DPolygon& aCandB(rCandidate.getB2DPolygon(b)); + StripHelper& rHelperB = aHelpers[b]; + const bool bAInB(rHelperB.maRange.isInside(rHelperA.maRange) && utils::isInside(aCandB, aCandA, true)); + const bool bBInA(rHelperA.maRange.isInside(rHelperB.maRange) && utils::isInside(aCandA, aCandB, true)); + + if(bAInB && bBInA) + { + // congruent + if(rHelperA.meOrinetation == rHelperB.meOrinetation) + { + // two polys or two holes. Lower one of them to get one of them out of the way. + // Since each will be contained in the other one, both will be increased, too. + // So, for lowering, increase only one of them + rHelperA.mnDepth++; + } + else + { + // poly and hole. They neutralize, so get rid of both. Move securely below zero. + rHelperA.mnDepth = - static_cast<sal_Int32>(nCount); + rHelperB.mnDepth = - static_cast<sal_Int32>(nCount); + } + } + else + { + if(bAInB) + { + if(rHelperB.meOrinetation == B2VectorOrientation::Negative) + { + rHelperA.mnDepth--; + } + else + { + rHelperA.mnDepth++; + } + } + else if(bBInA) + { + if(rHelperA.meOrinetation == B2VectorOrientation::Negative) + { + rHelperB.mnDepth--; + } + else + { + rHelperB.mnDepth++; + } + } + } + } + } + + for(a = 0; a < nCount; a++) + { + const StripHelper& rHelper = aHelpers[a]; + bool bAcceptEntry(bKeepAboveZero ? 1 <= rHelper.mnDepth : rHelper.mnDepth == 0); + + if(bAcceptEntry) + { + aRetval.append(rCandidate.getB2DPolygon(a)); + } + } + } + } + + return aRetval; + } + + B2DPolyPolygon prepareForPolygonOperation(const B2DPolygon& rCandidate) + { + solver aSolver(rCandidate); + B2DPolyPolygon aRetval(stripNeutralPolygons(aSolver.getB2DPolyPolygon())); + + return correctOrientations(aRetval); + } + + B2DPolyPolygon prepareForPolygonOperation(const B2DPolyPolygon& rCandidate) + { + solver aSolver(rCandidate); + B2DPolyPolygon aRetval(stripNeutralPolygons(aSolver.getB2DPolyPolygon())); + + return correctOrientations(aRetval); + } + + B2DPolyPolygon solvePolygonOperationOr(const B2DPolyPolygon& rCandidateA, const B2DPolyPolygon& rCandidateB) + { + if(!rCandidateA.count()) + { + return rCandidateB; + } + else if(!rCandidateB.count()) + { + return rCandidateA; + } + else + { + // concatenate polygons, solve crossovers and throw away all sub-polygons + // which have a depth other than 0. + B2DPolyPolygon aRetval(rCandidateA); + + aRetval.append(rCandidateB); + aRetval = solveCrossovers(aRetval); + aRetval = stripNeutralPolygons(aRetval); + + return stripDispensablePolygons(aRetval); + } + } + + B2DPolyPolygon solvePolygonOperationXor(const B2DPolyPolygon& rCandidateA, const B2DPolyPolygon& rCandidateB) + { + if(!rCandidateA.count()) + { + return rCandidateB; + } + else if(!rCandidateB.count()) + { + return rCandidateA; + } + else + { + // XOR is pretty simple: By definition it is the simple concatenation of + // the single polygons since we imply XOR fill rule. Make it intersection-free + // and correct orientations + B2DPolyPolygon aRetval(rCandidateA); + + aRetval.append(rCandidateB); + aRetval = solveCrossovers(aRetval); + aRetval = stripNeutralPolygons(aRetval); + + return correctOrientations(aRetval); + } + } + + B2DPolyPolygon solvePolygonOperationAnd(const B2DPolyPolygon& rCandidateA, const B2DPolyPolygon& rCandidateB) + { + if(!rCandidateA.count()) + { + return B2DPolyPolygon(); + } + else if(!rCandidateB.count()) + { + return B2DPolyPolygon(); + } + else + { + // tdf#130150 shortcut & precision: If both are simple ranges, + // solve based on ranges + if(basegfx::utils::isRectangle(rCandidateA) && basegfx::utils::isRectangle(rCandidateB)) + { + // *if* both are ranges, AND always can be solved + const basegfx::B2DRange aRangeA(rCandidateA.getB2DRange()); + const basegfx::B2DRange aRangeB(rCandidateB.getB2DRange()); + + if(aRangeA.isInside(aRangeB)) + { + // 2nd completely inside 1st -> 2nd is result of AND + return rCandidateB; + } + + if(aRangeB.isInside(aRangeA)) + { + // 2nd completely inside 1st -> 2nd is result of AND + return rCandidateA; + } + + // solve by intersection + basegfx::B2DRange aIntersect(aRangeA); + aIntersect.intersect(aRangeB); + + if(aIntersect.isEmpty()) + { + // no overlap -> empty polygon as result of AND + return B2DPolyPolygon(); + } + + // create polygon result + return B2DPolyPolygon( + basegfx::utils::createPolygonFromRect( + aIntersect)); + } + + // concatenate polygons, solve crossovers and throw away all sub-polygons + // with a depth of < 1. This means to keep all polygons where at least two + // polygons do overlap. + B2DPolyPolygon aRetval(rCandidateA); + + aRetval.append(rCandidateB); + aRetval = solveCrossovers(aRetval); + aRetval = stripNeutralPolygons(aRetval); + + return stripDispensablePolygons(aRetval, true); + } + } + + B2DPolyPolygon solvePolygonOperationDiff(const B2DPolyPolygon& rCandidateA, const B2DPolyPolygon& rCandidateB) + { + if(!rCandidateA.count()) + { + return B2DPolyPolygon(); + } + else if(!rCandidateB.count()) + { + return rCandidateA; + } + else + { + // Make B topologically to holes and append to A + B2DPolyPolygon aRetval(rCandidateB); + + aRetval.flip(); + aRetval.append(rCandidateA); + + // solve crossovers and throw away all sub-polygons which have a + // depth other than 0. + aRetval = basegfx::utils::solveCrossovers(aRetval); + aRetval = basegfx::utils::stripNeutralPolygons(aRetval); + + return basegfx::utils::stripDispensablePolygons(aRetval); + } + } + + B2DPolyPolygon mergeToSinglePolyPolygon(const B2DPolyPolygonVector& rInput) + { + if(rInput.empty()) + return B2DPolyPolygon(); + + // first step: prepareForPolygonOperation and simple merge of non-overlapping + // PolyPolygons for speedup; this is possible for the wanted OR-operation + B2DPolyPolygonVector aResult; + aResult.reserve(rInput.size()); + + for(const basegfx::B2DPolyPolygon & a : rInput) + { + const basegfx::B2DPolyPolygon aCandidate(prepareForPolygonOperation(a)); + + if(!aResult.empty()) + { + const B2DRange aCandidateRange(aCandidate.getB2DRange()); + bool bCouldMergeSimple(false); + + for(auto & b: aResult) + { + basegfx::B2DPolyPolygon aTarget(b); + const B2DRange aTargetRange(aTarget.getB2DRange()); + + if(!aCandidateRange.overlaps(aTargetRange)) + { + aTarget.append(aCandidate); + b = aTarget; + bCouldMergeSimple = true; + break; + } + } + + if(!bCouldMergeSimple) + { + aResult.push_back(aCandidate); + } + } + else + { + aResult.push_back(aCandidate); + } + } + + // second step: melt pairwise to a single PolyPolygon + while(aResult.size() > 1) + { + B2DPolyPolygonVector aResult2; + aResult2.reserve((aResult.size() / 2) + 1); + + for(size_t a(0); a < aResult.size(); a += 2) + { + if(a + 1 < aResult.size()) + { + // a pair for processing + aResult2.push_back(solvePolygonOperationOr(aResult[a], aResult[a + 1])); + } + else + { + // last single PolyPolygon; copy to target to not lose it + aResult2.push_back(aResult[a]); + } + } + + aResult = aResult2; + } + + // third step: get result + if(aResult.size() == 1) + { + return aResult[0]; + } + + return B2DPolyPolygon(); + } + +} // end of namespace + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/basegfx/source/polygon/b2dpolypolygontools.cxx b/basegfx/source/polygon/b2dpolypolygontools.cxx new file mode 100644 index 0000000000..faf734f6e7 --- /dev/null +++ b/basegfx/source/polygon/b2dpolypolygontools.cxx @@ -0,0 +1,657 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * 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 . + */ + +#include <basegfx/polygon/b2dpolypolygontools.hxx> +#include <osl/diagnose.h> +#include <com/sun/star/drawing/PolyPolygonBezierCoords.hpp> +#include <basegfx/polygon/b2dpolypolygon.hxx> +#include <basegfx/polygon/b2dpolygon.hxx> +#include <basegfx/polygon/b2dpolygontools.hxx> +#include <basegfx/numeric/ftools.hxx> +#include <rtl/math.hxx> + +#include <algorithm> +#include <numeric> + +namespace basegfx::utils +{ + B2DPolyPolygon correctOrientations(const B2DPolyPolygon& rCandidate) + { + B2DPolyPolygon aRetval(rCandidate); + const sal_uInt32 nCount(aRetval.count()); + + for(sal_uInt32 a(0); a < nCount; a++) + { + const B2DPolygon& aCandidate(rCandidate.getB2DPolygon(a)); + const B2VectorOrientation aOrientation(utils::getOrientation(aCandidate)); + sal_uInt32 nDepth(0); + + for(sal_uInt32 b(0); b < nCount; b++) + { + if(b != a) + { + const B2DPolygon& aCompare(rCandidate.getB2DPolygon(b)); + + if(utils::isInside(aCompare, aCandidate, true)) + { + nDepth++; + } + } + } + + const bool bShallBeHole((nDepth & 0x00000001) == 1); + const bool bIsHole(aOrientation == B2VectorOrientation::Negative); + + if(bShallBeHole != bIsHole && aOrientation != B2VectorOrientation::Neutral) + { + B2DPolygon aFlipped(aCandidate); + aFlipped.flip(); + aRetval.setB2DPolygon(a, aFlipped); + } + } + + return aRetval; + } + + B2DPolyPolygon correctOutmostPolygon(const B2DPolyPolygon& rCandidate) + { + const sal_uInt32 nCount(rCandidate.count()); + + if(nCount > 1) + { + for(sal_uInt32 a(0); a < nCount; a++) + { + const B2DPolygon& aCandidate(rCandidate.getB2DPolygon(a)); + sal_uInt32 nDepth(0); + + for(sal_uInt32 b(0); b < nCount; b++) + { + if(b != a) + { + const B2DPolygon& aCompare(rCandidate.getB2DPolygon(b)); + + if(utils::isInside(aCompare, aCandidate, true)) + { + nDepth++; + } + } + } + + if(!nDepth) + { + B2DPolyPolygon aRetval(rCandidate); + + if(a != 0) + { + // exchange polygon a and polygon 0 + aRetval.setB2DPolygon(0, aCandidate); + aRetval.setB2DPolygon(a, rCandidate.getB2DPolygon(0)); + } + + // exit + return aRetval; + } + } + } + + return rCandidate; + } + + B2DPolyPolygon adaptiveSubdivideByDistance(const B2DPolyPolygon& rCandidate, double fDistanceBound, int nRecurseLimit) + { + if(rCandidate.areControlPointsUsed()) + { + B2DPolyPolygon aRetval; + + for(auto const& rPolygon : rCandidate) + { + if(rPolygon.areControlPointsUsed()) + { + aRetval.append(utils::adaptiveSubdivideByDistance(rPolygon, fDistanceBound, nRecurseLimit)); + } + else + { + aRetval.append(rPolygon); + } + } + + return aRetval; + } + else + { + return rCandidate; + } + } + + B2DPolyPolygon adaptiveSubdivideByAngle(const B2DPolyPolygon& rCandidate, double fAngleBound) + { + if(rCandidate.areControlPointsUsed()) + { + B2DPolyPolygon aRetval; + + for(auto const& rPolygon : rCandidate) + { + if(rPolygon.areControlPointsUsed()) + { + aRetval.append(utils::adaptiveSubdivideByAngle(rPolygon, fAngleBound)); + } + else + { + aRetval.append(rPolygon); + } + } + + return aRetval; + } + else + { + return rCandidate; + } + } + + bool isInside(const B2DPolyPolygon& rCandidate, const B2DPoint& rPoint, bool bWithBorder) + { + if(rCandidate.count() == 1) + { + return isInside(rCandidate.getB2DPolygon(0), rPoint, bWithBorder); + } + else + { + sal_Int32 nInsideCount = std::count_if(rCandidate.begin(), rCandidate.end(), [rPoint, bWithBorder](B2DPolygon polygon){ return isInside(polygon, rPoint, bWithBorder); }); + + return (nInsideCount % 2); + } + } + + B2DRange getRange(const B2DPolyPolygon& rCandidate) + { + B2DRange aRetval; + + for(auto const& rPolygon : rCandidate) + { + aRetval.expand(utils::getRange(rPolygon)); + } + + return aRetval; + } + + double getSignedArea(const B2DPolyPolygon& rCandidate) + { + double fRetval(0.0); + + for(auto const& rPolygon : rCandidate) + { + fRetval += utils::getSignedArea(rPolygon); + } + + return fRetval; + } + + double getArea(const B2DPolyPolygon& rCandidate) + { + return fabs(getSignedArea(rCandidate)); + } + + void applyLineDashing(const B2DPolyPolygon& rCandidate, const std::vector<double>& rDotDashArray, B2DPolyPolygon* pLineTarget, double fFullDashDotLen) + { + if(fFullDashDotLen == 0.0 && !rDotDashArray.empty()) + { + // calculate fFullDashDotLen from rDotDashArray + fFullDashDotLen = std::accumulate(rDotDashArray.begin(), rDotDashArray.end(), 0.0); + } + + if(!(rCandidate.count() && fFullDashDotLen > 0.0)) + return; + + B2DPolyPolygon aLineTarget; + + for(auto const& rPolygon : rCandidate) + { + applyLineDashing( + rPolygon, + rDotDashArray, + pLineTarget ? &aLineTarget : nullptr, + nullptr, + fFullDashDotLen); + + if(pLineTarget) + { + pLineTarget->append(aLineTarget); + } + } + } + + bool isInEpsilonRange(const B2DPolyPolygon& rCandidate, const B2DPoint& rTestPosition, double fDistance) + { + for(auto const& rPolygon : rCandidate) + { + if(isInEpsilonRange(rPolygon, rTestPosition, fDistance)) + { + return true; + } + } + + return false; + } + + B3DPolyPolygon createB3DPolyPolygonFromB2DPolyPolygon(const B2DPolyPolygon& rCandidate, double fZCoordinate) + { + B3DPolyPolygon aRetval; + + for(auto const& rPolygon : rCandidate) + { + aRetval.append(createB3DPolygonFromB2DPolygon(rPolygon, fZCoordinate)); + } + + return aRetval; + } + + B2DPolyPolygon createB2DPolyPolygonFromB3DPolyPolygon(const B3DPolyPolygon& rCandidate, const B3DHomMatrix& rMat) + { + B2DPolyPolygon aRetval; + + for(auto const& rPolygon : rCandidate) + { + aRetval.append(createB2DPolygonFromB3DPolygon(rPolygon, rMat)); + } + + return aRetval; + } + + double getSmallestDistancePointToPolyPolygon(const B2DPolyPolygon& rCandidate, const B2DPoint& rTestPoint, sal_uInt32& rPolygonIndex, sal_uInt32& rEdgeIndex, double& rCut) + { + double fRetval(DBL_MAX); + const double fZero(0.0); + const sal_uInt32 nPolygonCount(rCandidate.count()); + + for(sal_uInt32 a(0); a < nPolygonCount; a++) + { + const B2DPolygon& aCandidate(rCandidate.getB2DPolygon(a)); + sal_uInt32 nNewEdgeIndex; + double fNewCut(0.0); + const double fNewDistance(getSmallestDistancePointToPolygon(aCandidate, rTestPoint, nNewEdgeIndex, fNewCut)); + + if(fRetval == DBL_MAX || fNewDistance < fRetval) + { + fRetval = fNewDistance; + rPolygonIndex = a; + rEdgeIndex = nNewEdgeIndex; + rCut = fNewCut; + + if(fTools::equal(fRetval, fZero)) + { + // already found zero distance, cannot get better. Ensure numerical zero value and end loop. + fRetval = 0.0; + break; + } + } + } + + return fRetval; + } + + B2DPolyPolygon distort(const B2DPolyPolygon& rCandidate, const B2DRange& rOriginal, const B2DPoint& rTopLeft, const B2DPoint& rTopRight, const B2DPoint& rBottomLeft, const B2DPoint& rBottomRight) + { + B2DPolyPolygon aRetval; + + for(auto const& rPolygon : rCandidate) + { + aRetval.append(distort(rPolygon, rOriginal, rTopLeft, rTopRight, rBottomLeft, rBottomRight)); + } + + return aRetval; + } + + B2DPolyPolygon expandToCurve(const B2DPolyPolygon& rCandidate) + { + B2DPolyPolygon aRetval; + + for(auto const& rPolygon : rCandidate) + { + aRetval.append(expandToCurve(rPolygon)); + } + + return aRetval; + } + + B2DPolyPolygon growInNormalDirection(const B2DPolyPolygon& rCandidate, double fValue) + { + if(fValue != 0.0) + { + B2DPolyPolygon aRetval; + + for(auto const& rPolygon : rCandidate) + { + aRetval.append(growInNormalDirection(rPolygon, fValue)); + } + + return aRetval; + } + else + { + return rCandidate; + } + } + + B2DPolyPolygon reSegmentPolyPolygon(const B2DPolyPolygon& rCandidate, sal_uInt32 nSegments) + { + B2DPolyPolygon aRetval; + + for(auto const& rPolygon : rCandidate) + { + aRetval.append(reSegmentPolygon(rPolygon, nSegments)); + } + + return aRetval; + } + + B2DPolyPolygon interpolate(const B2DPolyPolygon& rOld1, const B2DPolyPolygon& rOld2, double t) + { + OSL_ENSURE(rOld1.count() == rOld2.count(), "B2DPolyPolygon interpolate: Different geometry (!)"); + B2DPolyPolygon aRetval; + + for(sal_uInt32 a(0); a < rOld1.count(); a++) + { + aRetval.append(interpolate(rOld1.getB2DPolygon(a), rOld2.getB2DPolygon(a), t)); + } + + return aRetval; + } + + bool isRectangle( const B2DPolyPolygon& rPoly ) + { + // exclude some cheap cases first + if( rPoly.count() != 1 ) + return false; + + return isRectangle( rPoly.getB2DPolygon(0) ); + } + + // #i76891# + B2DPolyPolygon simplifyCurveSegments(const B2DPolyPolygon& rCandidate) + { + if(rCandidate.areControlPointsUsed()) + { + B2DPolyPolygon aRetval; + + for(auto const& rPolygon : rCandidate) + { + aRetval.append(simplifyCurveSegments(rPolygon)); + } + + return aRetval; + } + else + { + return rCandidate; + } + } + + B2DPolyPolygon snapPointsOfHorizontalOrVerticalEdges(const B2DPolyPolygon& rCandidate) + { + B2DPolyPolygon aRetval; + + for(auto const& rPolygon : rCandidate) + { + aRetval.append(snapPointsOfHorizontalOrVerticalEdges(rPolygon)); + } + + return aRetval; + } + + B2DPolyPolygon createSevenSegmentPolyPolygon(char nNumber, bool bLitSegments) + { + // config here + // { + const double fTotalSize=1.0; + const double fPosMiddleSegment=0.6; + const double fSegmentEndChopHoriz=0.08; + const double fSegmentEndChopVert =0.04; + // } + // config here + + const double fLeft=0.0; + const double fRight=fTotalSize; + const double fTop=0.0; + const double fMiddle=fPosMiddleSegment; + const double fBottom=fTotalSize; + + // from 0 to 5: pair of segment corner coordinates + + // segment corner indices are these: + + // 0 - 1 + // | | + // 2 - 3 + // | | + // 4 - 5 + + static const double corners[] = + { + fLeft, fTop, + fRight, fTop, + fLeft, fMiddle, + fRight, fMiddle, + fLeft, fBottom, + fRight, fBottom + }; + + // from 0 to 9: which segments are 'lit' for this number? + + // array denotes graph edges to traverse, with -1 means + // stop (the vertices are the corner indices from above): + // 0 + // - + // 1 | | 2 + // - 3 + // 4 | | 5 + // - + // 6 + + static const int numbers[] = + { + 1, 1, 1, 0, 1, 1, 1, // 0 + 0, 0, 1, 0, 0, 1, 0, // 1 + 1, 0, 1, 1, 1, 0, 1, // 2 + 1, 0, 1, 1, 0, 1, 1, // 3 + 0, 1, 1, 1, 0, 1, 0, // 4 + 1, 1, 0, 1, 0, 1, 1, // 5 + 1, 1, 0, 1, 1, 1, 1, // 6 + 1, 0, 1, 0, 0, 1, 0, // 1 + 1, 1, 1, 1, 1, 1, 1, // 8 + 1, 1, 1, 1, 0, 1, 1, // 9 + 0, 0, 0, 1, 0, 0, 0, // '-' + 1, 1, 0, 1, 1, 0, 1, // 'E' + }; + + // maps segment index to two corner ids: + static const int index2corner[] = + { + 0, 2, // 0 + 0, 4, // 1 + 2, 6, // 2 + 4, 6, // 3 + 4, 8, // 4 + 6, 10, // 5 + 8, 10, // 6 + }; + + B2DPolyPolygon aRes; + if( nNumber == '-' ) + { + nNumber = 10; + } + else if( nNumber == 'E' ) + { + nNumber = 11; + } + else if( nNumber == '.' ) + { + if( bLitSegments ) + aRes.append(createPolygonFromCircle(B2DPoint(fTotalSize/2, fTotalSize), + fSegmentEndChopHoriz)); + return aRes; + } + else + { + nNumber=std::clamp<sal_uInt32>(nNumber,'0','9') - '0'; + } + + B2DPolygon aCurrSegment; + const size_t sliceSize=std::size(numbers)/12; + const int* pCurrSegment=numbers + nNumber*sliceSize; + for( size_t i=0; i<sliceSize; i++, pCurrSegment++) + { + if( !(*pCurrSegment ^ int(bLitSegments)) ) + { + const size_t j=2*i; + aCurrSegment.clear(); + B2DPoint start(corners[index2corner[j]], + corners[index2corner[j]+1] ); + B2DPoint end (corners[index2corner[j+1]], + corners[index2corner[j+1]+1]); + + if( rtl::math::approxEqual(start.getX(), end.getX()) ) + { + start.setY(start.getY()+fSegmentEndChopVert); + end.setY(end.getY()-fSegmentEndChopVert); + } + else + { + start.setX(start.getX()+fSegmentEndChopHoriz); + end.setX(end.getX()-fSegmentEndChopHoriz); + } + + aCurrSegment.append(start); + aCurrSegment.append(end); + } + aRes.append(aCurrSegment); + } + + return aRes; + } + + // converters for css::drawing::PointSequence + + B2DPolyPolygon UnoPointSequenceSequenceToB2DPolyPolygon( + const css::drawing::PointSequenceSequence& rPointSequenceSequenceSource) + { + B2DPolyPolygon aRetval; + aRetval.reserve(rPointSequenceSequenceSource.getLength()); + const css::drawing::PointSequence* pPointSequence = rPointSequenceSequenceSource.getConstArray(); + const css::drawing::PointSequence* pPointSeqEnd = pPointSequence + rPointSequenceSequenceSource.getLength(); + + for(;pPointSequence != pPointSeqEnd; pPointSequence++) + { + const B2DPolygon aNewPolygon = UnoPointSequenceToB2DPolygon(*pPointSequence); + aRetval.append(aNewPolygon); + } + + return aRetval; + } + + void B2DPolyPolygonToUnoPointSequenceSequence( + const B2DPolyPolygon& rPolyPolygon, + css::drawing::PointSequenceSequence& rPointSequenceSequenceRetval) + { + const sal_uInt32 nCount(rPolyPolygon.count()); + + if(nCount) + { + rPointSequenceSequenceRetval.realloc(nCount); + css::drawing::PointSequence* pPointSequence = rPointSequenceSequenceRetval.getArray(); + + for(auto const& rPolygon : rPolyPolygon) + { + B2DPolygonToUnoPointSequence(rPolygon, *pPointSequence); + pPointSequence++; + } + } + else + { + rPointSequenceSequenceRetval.realloc(0); + } + } + + // converters for css::drawing::PolyPolygonBezierCoords (curved polygons) + + B2DPolyPolygon UnoPolyPolygonBezierCoordsToB2DPolyPolygon( + const css::drawing::PolyPolygonBezierCoords& rPolyPolygonBezierCoordsSource) + { + B2DPolyPolygon aRetval; + const sal_uInt32 nSequenceCount(static_cast<sal_uInt32>(rPolyPolygonBezierCoordsSource.Coordinates.getLength())); + + if(nSequenceCount) + { + OSL_ENSURE(nSequenceCount == static_cast<sal_uInt32>(rPolyPolygonBezierCoordsSource.Flags.getLength()), + "UnoPolyPolygonBezierCoordsToB2DPolyPolygon: unequal number of Points and Flags (!)"); + const css::drawing::PointSequence* pPointSequence = rPolyPolygonBezierCoordsSource.Coordinates.getConstArray(); + const css::drawing::FlagSequence* pFlagSequence = rPolyPolygonBezierCoordsSource.Flags.getConstArray(); + + for(sal_uInt32 a(0); a < nSequenceCount; a++) + { + const B2DPolygon aNewPolygon(UnoPolygonBezierCoordsToB2DPolygon( + *pPointSequence, + *pFlagSequence)); + + pPointSequence++; + pFlagSequence++; + aRetval.append(aNewPolygon); + } + } + + return aRetval; + } + + void B2DPolyPolygonToUnoPolyPolygonBezierCoords( + const B2DPolyPolygon& rPolyPolygon, + css::drawing::PolyPolygonBezierCoords& rPolyPolygonBezierCoordsRetval) + { + const sal_uInt32 nCount(rPolyPolygon.count()); + + if(nCount) + { + // prepare return value memory + rPolyPolygonBezierCoordsRetval.Coordinates.realloc(static_cast<sal_Int32>(nCount)); + rPolyPolygonBezierCoordsRetval.Flags.realloc(static_cast<sal_Int32>(nCount)); + + // get pointers to arrays + css::drawing::PointSequence* pPointSequence = rPolyPolygonBezierCoordsRetval.Coordinates.getArray(); + css::drawing::FlagSequence* pFlagSequence = rPolyPolygonBezierCoordsRetval.Flags.getArray(); + + for(auto const& rSource : rPolyPolygon) + { + B2DPolygonToUnoPolygonBezierCoords( + rSource, + *pPointSequence, + *pFlagSequence); + pPointSequence++; + pFlagSequence++; + } + } + else + { + rPolyPolygonBezierCoordsRetval.Coordinates.realloc(0); + rPolyPolygonBezierCoordsRetval.Flags.realloc(0); + } + } + +} // end of namespace + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/basegfx/source/polygon/b2dsvgpolypolygon.cxx b/basegfx/source/polygon/b2dsvgpolypolygon.cxx new file mode 100644 index 0000000000..fe4f646eb3 --- /dev/null +++ b/basegfx/source/polygon/b2dsvgpolypolygon.cxx @@ -0,0 +1,931 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * 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 . + */ + +#include <basegfx/polygon/b2dpolygontools.hxx> +#include <basegfx/polygon/b2dpolypolygontools.hxx> +#include <basegfx/polygon/b2dpolypolygon.hxx> +#include <basegfx/matrix/b2dhommatrix.hxx> +#include <basegfx/matrix/b2dhommatrixtools.hxx> + +#include <rtl/ustring.hxx> +#include <sal/log.hxx> +#include <rtl/math.hxx> +#include <rtl/character.hxx> +#include <stringconversiontools.hxx> + +namespace +{ + +void putCommandChar(OUStringBuffer& rBuffer,sal_Unicode& rLastSVGCommand, sal_Unicode aChar, bool bToLower,bool bVerbose) +{ + const sal_Unicode aCommand = bToLower ? rtl::toAsciiLowerCase(aChar) : aChar; + + if (bVerbose && rBuffer.getLength()) + rBuffer.append(' '); + + if (bVerbose || rLastSVGCommand != aCommand) + { + rBuffer.append(aCommand); + rLastSVGCommand = aCommand; + } +} + +void putNumberChar(OUStringBuffer& rStr,double fValue, double fOldValue, bool bUseRelativeCoordinates,bool bVerbose) +{ + if (bUseRelativeCoordinates) + fValue -= fOldValue; + + const sal_Int32 aLen(rStr.getLength()); + if (bVerbose || (aLen && basegfx::internal::isOnNumberChar(rStr[aLen - 1], false) && fValue >= 0.0)) + rStr.append(' '); + + rStr.append(fValue); +} + +} + +namespace basegfx::utils +{ + bool PointIndex::operator<(const PointIndex& rComp) const + { + if(rComp.getPolygonIndex() == getPolygonIndex()) + { + return rComp.getPointIndex() < getPointIndex(); + } + + return rComp.getPolygonIndex() < getPolygonIndex(); + } + + bool importFromSvgD( + B2DPolyPolygon& o_rPolyPolygon, + std::u16string_view rSvgDStatement, + bool bHandleRelativeNextPointCompatible, + PointIndexSet* pHelpPointIndexSet) + { + o_rPolyPolygon.clear(); + const sal_Int32 nLen(rSvgDStatement.size()); + sal_Int32 nPos(0); + double nLastX( 0.0 ); + double nLastY( 0.0 ); + B2DPolygon aCurrPoly; + + // skip initial whitespace + basegfx::internal::skipSpaces(nPos, rSvgDStatement, nLen); + + while(nPos < nLen) + { + bool bRelative(false); + const sal_Unicode aCurrChar(rSvgDStatement[nPos]); + + if(o_rPolyPolygon.count() && !aCurrPoly.count() && aCurrChar != 'm' && aCurrChar != 'M') + { + // we have a new sub-polygon starting, but without a 'moveto' command. + // this requires to add the current point as start point to the polygon + // (see SVG1.1 8.3.3 The "closepath" command) + aCurrPoly.append(B2DPoint(nLastX, nLastY)); + } + + switch(aCurrChar) + { + case 'z' : + case 'Z' : + { + // consume CurrChar and whitespace + nPos++; + basegfx::internal::skipSpaces(nPos, rSvgDStatement, nLen); + + // create closed polygon and reset import values + if(aCurrPoly.count()) + { + if(!bHandleRelativeNextPointCompatible) + { + // SVG defines that "the next subpath starts at the + // same initial point as the current subpath", so set the + // current point if we do not need to be compatible + nLastX = aCurrPoly.getB2DPoint(0).getX(); + nLastY = aCurrPoly.getB2DPoint(0).getY(); + } + + aCurrPoly.setClosed(true); + o_rPolyPolygon.append(aCurrPoly); + aCurrPoly.clear(); + } + + break; + } + + case 'm' : + case 'M' : + { + // create non-closed polygon and reset import values + if(aCurrPoly.count()) + { + o_rPolyPolygon.append(aCurrPoly); + aCurrPoly.clear(); + } + [[fallthrough]]; // to add coordinate data as 1st point of new polygon + } + case 'l' : + case 'L' : + { + if(aCurrChar == 'm' || aCurrChar == 'l') + { + bRelative = true; + } + + // consume CurrChar and whitespace + nPos++; + basegfx::internal::skipSpaces(nPos, rSvgDStatement, nLen); + + while(nPos < nLen && basegfx::internal::isOnNumberChar(rSvgDStatement, nPos)) + { + double nX, nY; + + if(!basegfx::internal::importDoubleAndSpaces(nX, nPos, rSvgDStatement, nLen)) return false; + if(!basegfx::internal::importDoubleAndSpaces(nY, nPos, rSvgDStatement, nLen)) return false; + + if(bRelative) + { + nX += nLastX; + nY += nLastY; + } + + // set last position + nLastX = nX; + nLastY = nY; + + // add point + aCurrPoly.append(B2DPoint(nX, nY)); + } + break; + } + + case 'h' : + { + bRelative = true; + [[fallthrough]]; + } + case 'H' : + { + nPos++; + basegfx::internal::skipSpaces(nPos, rSvgDStatement, nLen); + + while(nPos < nLen && basegfx::internal::isOnNumberChar(rSvgDStatement, nPos)) + { + double nX, nY(nLastY); + + if(!basegfx::internal::importDoubleAndSpaces(nX, nPos, rSvgDStatement, nLen)) return false; + + if(bRelative) + { + nX += nLastX; + } + + // set last position + nLastX = nX; + + // add point + aCurrPoly.append(B2DPoint(nX, nY)); + } + break; + } + + case 'v' : + { + bRelative = true; + [[fallthrough]]; + } + case 'V' : + { + nPos++; + basegfx::internal::skipSpaces(nPos, rSvgDStatement, nLen); + + while(nPos < nLen && basegfx::internal::isOnNumberChar(rSvgDStatement, nPos)) + { + double nX(nLastX), nY; + + if(!basegfx::internal::importDoubleAndSpaces(nY, nPos, rSvgDStatement, nLen)) return false; + + if(bRelative) + { + nY += nLastY; + } + + // set last position + nLastY = nY; + + // add point + aCurrPoly.append(B2DPoint(nX, nY)); + } + break; + } + + case 's' : + { + bRelative = true; + [[fallthrough]]; + } + case 'S' : + { + nPos++; + basegfx::internal::skipSpaces(nPos, rSvgDStatement, nLen); + + while(nPos < nLen && basegfx::internal::isOnNumberChar(rSvgDStatement, nPos)) + { + double nX, nY; + double nX2, nY2; + + if(!basegfx::internal::importDoubleAndSpaces(nX2, nPos, rSvgDStatement, nLen)) return false; + if(!basegfx::internal::importDoubleAndSpaces(nY2, nPos, rSvgDStatement, nLen)) return false; + if(!basegfx::internal::importDoubleAndSpaces(nX, nPos, rSvgDStatement, nLen)) return false; + if(!basegfx::internal::importDoubleAndSpaces(nY, nPos, rSvgDStatement, nLen)) return false; + + if(bRelative) + { + nX2 += nLastX; + nY2 += nLastY; + nX += nLastX; + nY += nLastY; + } + + // ensure existence of start point + if(!aCurrPoly.count()) + { + aCurrPoly.append(B2DPoint(nLastX, nLastY)); + } + + // get first control point. It's the reflection of the PrevControlPoint + // of the last point. If not existent, use current point (see SVG) + B2DPoint aPrevControl(nLastX, nLastY); + const sal_uInt32 nIndex(aCurrPoly.count() - 1); + + if(aCurrPoly.areControlPointsUsed() && aCurrPoly.isPrevControlPointUsed(nIndex)) + { + const B2DPoint aPrevPoint(aCurrPoly.getB2DPoint(nIndex)); + const B2DPoint aPrevControlPoint(aCurrPoly.getPrevControlPoint(nIndex)); + + // use mirrored previous control point + aPrevControl.setX((2.0 * aPrevPoint.getX()) - aPrevControlPoint.getX()); + aPrevControl.setY((2.0 * aPrevPoint.getY()) - aPrevControlPoint.getY()); + } + + // append curved edge + aCurrPoly.appendBezierSegment(aPrevControl, B2DPoint(nX2, nY2), B2DPoint(nX, nY)); + + // set last position + nLastX = nX; + nLastY = nY; + } + break; + } + + case 'c' : + { + bRelative = true; + [[fallthrough]]; + } + case 'C' : + { + nPos++; + basegfx::internal::skipSpaces(nPos, rSvgDStatement, nLen); + + while(nPos < nLen && basegfx::internal::isOnNumberChar(rSvgDStatement, nPos)) + { + double nX, nY; + double nX1, nY1; + double nX2, nY2; + + if(!basegfx::internal::importDoubleAndSpaces(nX1, nPos, rSvgDStatement, nLen)) return false; + if(!basegfx::internal::importDoubleAndSpaces(nY1, nPos, rSvgDStatement, nLen)) return false; + if(!basegfx::internal::importDoubleAndSpaces(nX2, nPos, rSvgDStatement, nLen)) return false; + if(!basegfx::internal::importDoubleAndSpaces(nY2, nPos, rSvgDStatement, nLen)) return false; + if(!basegfx::internal::importDoubleAndSpaces(nX, nPos, rSvgDStatement, nLen)) return false; + if(!basegfx::internal::importDoubleAndSpaces(nY, nPos, rSvgDStatement, nLen)) return false; + + if(bRelative) + { + nX1 += nLastX; + nY1 += nLastY; + nX2 += nLastX; + nY2 += nLastY; + nX += nLastX; + nY += nLastY; + } + + // ensure existence of start point + if(!aCurrPoly.count()) + { + aCurrPoly.append(B2DPoint(nLastX, nLastY)); + } + + // append curved edge + aCurrPoly.appendBezierSegment(B2DPoint(nX1, nY1), B2DPoint(nX2, nY2), B2DPoint(nX, nY)); + + // set last position + nLastX = nX; + nLastY = nY; + } + break; + } + + // #100617# quadratic beziers are imported as cubic ones + case 'q' : + { + bRelative = true; + [[fallthrough]]; + } + case 'Q' : + { + nPos++; + basegfx::internal::skipSpaces(nPos, rSvgDStatement, nLen); + + while(nPos < nLen && basegfx::internal::isOnNumberChar(rSvgDStatement, nPos)) + { + double nX, nY; + double nX1, nY1; + + if(!basegfx::internal::importDoubleAndSpaces(nX1, nPos, rSvgDStatement, nLen)) return false; + if(!basegfx::internal::importDoubleAndSpaces(nY1, nPos, rSvgDStatement, nLen)) return false; + if(!basegfx::internal::importDoubleAndSpaces(nX, nPos, rSvgDStatement, nLen)) return false; + if(!basegfx::internal::importDoubleAndSpaces(nY, nPos, rSvgDStatement, nLen)) return false; + + if(bRelative) + { + nX1 += nLastX; + nY1 += nLastY; + nX += nLastX; + nY += nLastY; + } + + // ensure existence of start point + if(!aCurrPoly.count()) + { + aCurrPoly.append(B2DPoint(nLastX, nLastY)); + } + + // append curved edge + aCurrPoly.appendQuadraticBezierSegment(B2DPoint(nX1, nY1), B2DPoint(nX, nY)); + + // set last position + nLastX = nX; + nLastY = nY; + } + break; + } + + // #100617# relative quadratic beziers are imported as cubic + case 't' : + { + bRelative = true; + [[fallthrough]]; + } + case 'T' : + { + nPos++; + basegfx::internal::skipSpaces(nPos, rSvgDStatement, nLen); + + while(nPos < nLen && basegfx::internal::isOnNumberChar(rSvgDStatement, nPos)) + { + double nX, nY; + + if(!basegfx::internal::importDoubleAndSpaces(nX, nPos, rSvgDStatement, nLen)) return false; + if(!basegfx::internal::importDoubleAndSpaces(nY, nPos, rSvgDStatement, nLen)) return false; + + if(bRelative) + { + nX += nLastX; + nY += nLastY; + } + + // ensure existence of start point + if(!aCurrPoly.count()) + { + aCurrPoly.append(B2DPoint(nLastX, nLastY)); + } + + // get first control point. It's the reflection of the PrevControlPoint + // of the last point. If not existent, use current point (see SVG) + B2DPoint aPrevControl(nLastX, nLastY); + const sal_uInt32 nIndex(aCurrPoly.count() - 1); + const B2DPoint aPrevPoint(aCurrPoly.getB2DPoint(nIndex)); + + if(aCurrPoly.areControlPointsUsed() && aCurrPoly.isPrevControlPointUsed(nIndex)) + { + const B2DPoint aPrevControlPoint(aCurrPoly.getPrevControlPoint(nIndex)); + + // use mirrored previous control point + aPrevControl.setX((2.0 * aPrevPoint.getX()) - aPrevControlPoint.getX()); + aPrevControl.setY((2.0 * aPrevPoint.getY()) - aPrevControlPoint.getY()); + } + + if(!aPrevControl.equal(aPrevPoint)) + { + // there is a prev control point, and we have the already mirrored one + // in aPrevControl. We also need the quadratic control point for this + // new quadratic segment to calculate the 2nd cubic control point + const B2DPoint aQuadControlPoint( + ((3.0 * aPrevControl.getX()) - aPrevPoint.getX()) / 2.0, + ((3.0 * aPrevControl.getY()) - aPrevPoint.getY()) / 2.0); + + // calculate the cubic bezier coefficients from the quadratic ones. + const double nX2Prime((aQuadControlPoint.getX() * 2.0 + nX) / 3.0); + const double nY2Prime((aQuadControlPoint.getY() * 2.0 + nY) / 3.0); + + // append curved edge, use mirrored cubic control point directly + aCurrPoly.appendBezierSegment(aPrevControl, B2DPoint(nX2Prime, nY2Prime), B2DPoint(nX, nY)); + } + else + { + // when no previous control, SVG says to use current point -> straight line. + // Just add end point + aCurrPoly.append(B2DPoint(nX, nY)); + } + + // set last position + nLastX = nX; + nLastY = nY; + } + break; + } + + case 'a' : + { + bRelative = true; + [[fallthrough]]; + } + case 'A' : + { + nPos++; + basegfx::internal::skipSpaces(nPos, rSvgDStatement, nLen); + + while(nPos < nLen && basegfx::internal::isOnNumberChar(rSvgDStatement, nPos)) + { + double nX, nY; + double fRX, fRY, fPhi; + sal_Int32 bLargeArcFlag, bSweepFlag; + + if(!basegfx::internal::importDoubleAndSpaces(fRX, nPos, rSvgDStatement, nLen)) return false; + if(!basegfx::internal::importDoubleAndSpaces(fRY, nPos, rSvgDStatement, nLen)) return false; + if(!basegfx::internal::importDoubleAndSpaces(fPhi, nPos, rSvgDStatement, nLen)) return false; + if(!basegfx::internal::importFlagAndSpaces(bLargeArcFlag, nPos, rSvgDStatement, nLen)) return false; + if(!basegfx::internal::importFlagAndSpaces(bSweepFlag, nPos, rSvgDStatement, nLen)) return false; + if(!basegfx::internal::importDoubleAndSpaces(nX, nPos, rSvgDStatement, nLen)) return false; + if(!basegfx::internal::importDoubleAndSpaces(nY, nPos, rSvgDStatement, nLen)) return false; + + if(bRelative) + { + nX += nLastX; + nY += nLastY; + } + + if( rtl::math::approxEqual(nX, nLastX) && rtl::math::approxEqual(nY, nLastY) ) + continue; // start==end -> skip according to SVG spec + + if( fRX == 0.0 || fRY == 0.0 ) + { + // straight line segment according to SVG spec + aCurrPoly.append(B2DPoint(nX, nY)); + } + else + { + // normalize according to SVG spec + fRX=fabs(fRX); fRY=fabs(fRY); + + // from the SVG spec, appendix F.6.4 + + // |x1'| |cos phi sin phi| |(x1 - x2)/2| + // |y1'| = |-sin phi cos phi| |(y1 - y2)/2| + const B2DPoint p1(nLastX, nLastY); + const B2DPoint p2(nX, nY); + B2DHomMatrix aTransform(basegfx::utils::createRotateB2DHomMatrix( + -deg2rad(fPhi))); + + const B2DPoint p1_prime( aTransform * B2DPoint(((p1-p2)/2.0)) ); + + // ______________________________________ rx y1' + // |cx'| + / rx^2 ry^2 - rx^2 y1'^2 - ry^2 x1^2 ry + // |cy'| =-/ rx^2y1'^2 + ry^2 x1'^2 - ry x1' + // rx + // chose + if f_A != f_S + // chose - if f_A = f_S + B2DPoint aCenter_prime; + const double fRadicant( + (fRX*fRX*fRY*fRY - fRX*fRX*p1_prime.getY()*p1_prime.getY() - fRY*fRY*p1_prime.getX()*p1_prime.getX())/ + (fRX*fRX*p1_prime.getY()*p1_prime.getY() + fRY*fRY*p1_prime.getX()*p1_prime.getX())); + if( fRadicant < 0.0 ) + { + // no solution - according to SVG + // spec, scale up ellipse + // uniformly such that it passes + // through end points (denominator + // of radicant solved for fRY, + // with s=fRX/fRY) + const double fRatio(fRX/fRY); + const double fRadicant2( + p1_prime.getY()*p1_prime.getY() + + p1_prime.getX()*p1_prime.getX()/(fRatio*fRatio)); + if( fRadicant2 < 0.0 ) + { + // only trivial solution, one + // of the axes 0 -> straight + // line segment according to + // SVG spec + aCurrPoly.append(B2DPoint(nX, nY)); + continue; + } + + fRY=sqrt(fRadicant2); + fRX=fRatio*fRY; + + // keep center_prime forced to (0,0) + } + else + { + const double fFactor( + (bLargeArcFlag==bSweepFlag ? -1.0 : 1.0) * + sqrt(fRadicant)); + + // actually calculate center_prime + aCenter_prime = B2DPoint( + fFactor*fRX*p1_prime.getY()/fRY, + -fFactor*fRY*p1_prime.getX()/fRX); + } + + // + u - v + // angle(u,v) = arccos( ------------ ) (take the sign of (ux vy - uy vx)) + // - ||u|| ||v|| + + // 1 | (x1' - cx')/rx | + // theta1 = angle(( ), | | ) + // 0 | (y1' - cy')/ry | + const B2DPoint aRadii(fRX,fRY); + double fTheta1( + B2DVector(1.0,0.0).angle( + (p1_prime-aCenter_prime)/aRadii)); + + // |1| | (-x1' - cx')/rx | + // theta2 = angle( | | , | | ) + // |0| | (-y1' - cy')/ry | + double fTheta2( + B2DVector(1.0,0.0).angle( + (-p1_prime-aCenter_prime)/aRadii)); + + // map both angles to [0,2pi) + fTheta1 = fmod(2*M_PI+fTheta1,2*M_PI); + fTheta2 = fmod(2*M_PI+fTheta2,2*M_PI); + + // make sure the large arc is taken + // (since + // createPolygonFromEllipseSegment() + // normalizes to e.g. cw arc) + if( !bSweepFlag ) + std::swap(fTheta1,fTheta2); + + // finally, create bezier polygon from this + B2DPolygon aSegment( + utils::createPolygonFromUnitEllipseSegment( + fTheta1, fTheta2 )); + + // transform ellipse by rotation & move to final center + aTransform = basegfx::utils::createScaleB2DHomMatrix(fRX, fRY); + aTransform.translate(aCenter_prime.getX(), + aCenter_prime.getY()); + aTransform.rotate(deg2rad(fPhi)); + const B2DPoint aOffset((p1+p2)/2.0); + aTransform.translate(aOffset.getX(), + aOffset.getY()); + aSegment.transform(aTransform); + + // createPolygonFromEllipseSegment() + // always creates arcs that are + // positively oriented - flip polygon + // if we swapped angles above + if( !bSweepFlag ) + aSegment.flip(); + + // remember PointIndex of evtl. added pure helper points + sal_uInt32 nPointIndex(aCurrPoly.count() + 1); + aCurrPoly.append(aSegment); + + // if asked for, mark pure helper points by adding them to the index list of + // helper points + if(pHelpPointIndexSet && aCurrPoly.count() > 1) + { + const sal_uInt32 nPolyIndex(o_rPolyPolygon.count()); + + for(;nPointIndex + 1 < aCurrPoly.count(); nPointIndex++) + { + pHelpPointIndexSet->insert(PointIndex(nPolyIndex, nPointIndex)); + } + } + } + + // set last position + nLastX = nX; + nLastY = nY; + } + break; + } + + default: + { + SAL_WARN("basegfx", "importFromSvgD(): skipping tags in svg:d element (unknown: \"" + << OUString(aCurrChar) + << "\")!"); + ++nPos; + break; + } + } + } + + // if there is polygon data, create non-closed polygon + if(aCurrPoly.count()) + { + o_rPolyPolygon.append(aCurrPoly); + } + + return true; + } + + bool importFromSvgPoints( B2DPolygon& o_rPoly, + std::u16string_view rSvgPointsAttribute ) + { + o_rPoly.clear(); + const sal_Int32 nLen(rSvgPointsAttribute.size()); + sal_Int32 nPos(0); + double nX, nY; + + // skip initial whitespace + basegfx::internal::skipSpaces(nPos, rSvgPointsAttribute, nLen); + + while(nPos < nLen) + { + if(!basegfx::internal::importDoubleAndSpaces(nX, nPos, rSvgPointsAttribute, nLen)) return false; + if(!basegfx::internal::importDoubleAndSpaces(nY, nPos, rSvgPointsAttribute, nLen)) return false; + + // add point + o_rPoly.append(B2DPoint(nX, nY)); + + // skip to next number, or finish + basegfx::internal::skipSpaces(nPos, rSvgPointsAttribute, nLen); + } + + return true; + } + + OUString exportToSvgPoints( const B2DPolygon& rPoly ) + { + SAL_WARN_IF(rPoly.areControlPointsUsed(), "basegfx", "exportToSvgPoints: Only non-bezier polygons allowed (!)"); + const sal_uInt32 nPointCount(rPoly.count()); + OUStringBuffer aResult; + + for(sal_uInt32 a(0); a < nPointCount; a++) + { + const basegfx::B2DPoint aPoint(rPoly.getB2DPoint(a)); + + if(a) + { + aResult.append(' '); + } + + aResult.append(OUString::number(aPoint.getX()) + + "," + + OUString::number(aPoint.getY())); + } + + return aResult.makeStringAndClear(); + } + + OUString exportToSvgD( + const B2DPolyPolygon& rPolyPolygon, + bool bUseRelativeCoordinates, + bool bDetectQuadraticBeziers, + bool bHandleRelativeNextPointCompatible, + bool bOOXMLMotionPath) + { + const sal_uInt32 nCount(rPolyPolygon.count()); + sal_uInt32 nCombinedPointCount = 0; + for(sal_uInt32 i(0); i < nCount; i++) + { + const B2DPolygon& aPolygon(rPolyPolygon.getB2DPolygon(i)); + nCombinedPointCount += aPolygon.count(); + } + + OUStringBuffer aResult(std::max<int>(nCombinedPointCount * 32,512)); + B2DPoint aCurrentSVGPosition(0.0, 0.0); // SVG assumes (0,0) as the initial current point + + for(sal_uInt32 i(0); i < nCount; i++) + { + const B2DPolygon& aPolygon(rPolyPolygon.getB2DPolygon(i)); + const sal_uInt32 nPointCount(aPolygon.count()); + + if(nPointCount) + { + const bool bPolyUsesControlPoints(aPolygon.areControlPointsUsed()); + const sal_uInt32 nEdgeCount(aPolygon.isClosed() ? nPointCount : nPointCount - 1); + sal_Unicode aLastSVGCommand(' '); // last SVG command char + B2DPoint aLeft, aRight; // for quadratic bezier test + + // handle polygon start point + B2DPoint aEdgeStart(aPolygon.getB2DPoint(0)); + bool bUseRelativeCoordinatesForFirstPoint(bUseRelativeCoordinates); + + if(bHandleRelativeNextPointCompatible) + { + // To get around the error that the start point for the next polygon is the + // start point of the current one (and not the last as it was handled up to now) + // do force to write an absolute 'M' command as start for the next polygon + bUseRelativeCoordinatesForFirstPoint = false; + } + + // Write 'moveto' and the 1st coordinates, set aLastSVGCommand to 'lineto' + putCommandChar(aResult, aLastSVGCommand, 'M', bUseRelativeCoordinatesForFirstPoint, bOOXMLMotionPath); + putNumberChar(aResult, aEdgeStart.getX(), aCurrentSVGPosition.getX(), bUseRelativeCoordinatesForFirstPoint, bOOXMLMotionPath); + putNumberChar(aResult, aEdgeStart.getY(), aCurrentSVGPosition.getY(), bUseRelativeCoordinatesForFirstPoint, bOOXMLMotionPath); + aLastSVGCommand = bUseRelativeCoordinatesForFirstPoint ? 'l' : 'L'; + aCurrentSVGPosition = aEdgeStart; + + for(sal_uInt32 nIndex(0); nIndex < nEdgeCount; nIndex++) + { + // prepare access to next point + const sal_uInt32 nNextIndex((nIndex + 1) % nPointCount); + const B2DPoint aEdgeEnd(aPolygon.getB2DPoint(nNextIndex)); + + // handle edge from (aEdgeStart, aEdgeEnd) using indices (nIndex, nNextIndex) + const bool bEdgeIsBezier(bPolyUsesControlPoints + && (aPolygon.isNextControlPointUsed(nIndex) || aPolygon.isPrevControlPointUsed(nNextIndex))); + + if(bEdgeIsBezier) + { + // handle bezier edge + const B2DPoint aControlEdgeStart(aPolygon.getNextControlPoint(nIndex)); + const B2DPoint aControlEdgeEnd(aPolygon.getPrevControlPoint(nNextIndex)); + bool bIsQuadraticBezier(false); + + // check continuity at current edge's start point. For SVG, do NOT use an + // existing continuity since no 'S' or 's' statement should be written. At + // import, that 'previous' control vector is not available. SVG documentation + // says for interpretation: + + // "(If there is no previous command or if the previous command was + // not a C, c, S or s, assume the first control point is coincident + // with the current point.)" + + // That's what is done from our import, so avoid exporting it as first statement + // is necessary. + const bool bSymmetricAtEdgeStart( + !bOOXMLMotionPath && nIndex != 0 + && aPolygon.getContinuityInPoint(nIndex) == B2VectorContinuity::C2); + + if(bDetectQuadraticBeziers) + { + // check for quadratic beziers - that's + // the case if both control points are in + // the same place when they are prolonged + // to the common quadratic control point + + // Left: P = (3P1 - P0) / 2 + // Right: P = (3P2 - P3) / 2 + aLeft = B2DPoint((3.0 * aControlEdgeStart - aEdgeStart) / 2.0); + aRight= B2DPoint((3.0 * aControlEdgeEnd - aEdgeEnd) / 2.0); + bIsQuadraticBezier = aLeft.equal(aRight); + } + + if(bIsQuadraticBezier) + { + // approximately equal, export as quadratic bezier + if(bSymmetricAtEdgeStart) + { + putCommandChar(aResult, aLastSVGCommand, 'T', bUseRelativeCoordinates, bOOXMLMotionPath); + + putNumberChar(aResult, aEdgeEnd.getX(), aCurrentSVGPosition.getX(), bUseRelativeCoordinates, bOOXMLMotionPath); + putNumberChar(aResult, aEdgeEnd.getY(), aCurrentSVGPosition.getY(), bUseRelativeCoordinates, bOOXMLMotionPath); + aCurrentSVGPosition = aEdgeEnd; + } + else + { + putCommandChar(aResult, aLastSVGCommand, 'Q', bUseRelativeCoordinates, bOOXMLMotionPath); + + putNumberChar(aResult, aLeft.getX(), aCurrentSVGPosition.getX(), bUseRelativeCoordinates, bOOXMLMotionPath); + putNumberChar(aResult, aLeft.getY(), aCurrentSVGPosition.getY(), bUseRelativeCoordinates, bOOXMLMotionPath); + putNumberChar(aResult, aEdgeEnd.getX(), aCurrentSVGPosition.getX(), bUseRelativeCoordinates, bOOXMLMotionPath); + putNumberChar(aResult, aEdgeEnd.getY(), aCurrentSVGPosition.getY(), bUseRelativeCoordinates, bOOXMLMotionPath); + aCurrentSVGPosition = aEdgeEnd; + } + } + else + { + // export as cubic bezier + if(bSymmetricAtEdgeStart) + { + putCommandChar(aResult, aLastSVGCommand, 'S', bUseRelativeCoordinates, bOOXMLMotionPath); + + putNumberChar(aResult, aControlEdgeEnd.getX(), aCurrentSVGPosition.getX(), bUseRelativeCoordinates, bOOXMLMotionPath); + putNumberChar(aResult, aControlEdgeEnd.getY(), aCurrentSVGPosition.getY(), bUseRelativeCoordinates, bOOXMLMotionPath); + putNumberChar(aResult, aEdgeEnd.getX(), aCurrentSVGPosition.getX(), bUseRelativeCoordinates, bOOXMLMotionPath); + putNumberChar(aResult, aEdgeEnd.getY(), aCurrentSVGPosition.getY(), bUseRelativeCoordinates, bOOXMLMotionPath); + aCurrentSVGPosition = aEdgeEnd; + } + else + { + putCommandChar(aResult, aLastSVGCommand, 'C', bUseRelativeCoordinates, bOOXMLMotionPath); + + putNumberChar(aResult, aControlEdgeStart.getX(), aCurrentSVGPosition.getX(), bUseRelativeCoordinates, bOOXMLMotionPath); + putNumberChar(aResult, aControlEdgeStart.getY(), aCurrentSVGPosition.getY(), bUseRelativeCoordinates, bOOXMLMotionPath); + putNumberChar(aResult, aControlEdgeEnd.getX(), aCurrentSVGPosition.getX(), bUseRelativeCoordinates, bOOXMLMotionPath); + putNumberChar(aResult, aControlEdgeEnd.getY(), aCurrentSVGPosition.getY(), bUseRelativeCoordinates, bOOXMLMotionPath); + putNumberChar(aResult, aEdgeEnd.getX(), aCurrentSVGPosition.getX(), bUseRelativeCoordinates, bOOXMLMotionPath); + putNumberChar(aResult, aEdgeEnd.getY(), aCurrentSVGPosition.getY(), bUseRelativeCoordinates, bOOXMLMotionPath); + aCurrentSVGPosition = aEdgeEnd; + } + } + } + else + { + // straight edge + if(nNextIndex == 0) + { + // it's a closed polygon's last edge and it's not a bezier edge, so there is + // no need to write it + } + else + { + const bool bXEqual(rtl::math::approxEqual(aEdgeStart.getX(), aEdgeEnd.getX())); + const bool bYEqual(rtl::math::approxEqual(aEdgeStart.getY(), aEdgeEnd.getY())); + + if(bXEqual && bYEqual) + { + // point is a double point; do not export at all + } + else if(bXEqual && !bOOXMLMotionPath) + { + // export as vertical line + putCommandChar(aResult, aLastSVGCommand, 'V', bUseRelativeCoordinates, bOOXMLMotionPath); + + putNumberChar(aResult, aEdgeEnd.getY(), aCurrentSVGPosition.getY(), bUseRelativeCoordinates, bOOXMLMotionPath); + aCurrentSVGPosition = aEdgeEnd; + } + else if(bYEqual && !bOOXMLMotionPath) + { + // export as horizontal line + putCommandChar(aResult, aLastSVGCommand, 'H', bUseRelativeCoordinates, bOOXMLMotionPath); + + putNumberChar(aResult, aEdgeEnd.getX(), aCurrentSVGPosition.getX(), bUseRelativeCoordinates, bOOXMLMotionPath); + aCurrentSVGPosition = aEdgeEnd; + } + else + { + // export as line + putCommandChar(aResult, aLastSVGCommand, 'L', bUseRelativeCoordinates, bOOXMLMotionPath); + + putNumberChar(aResult, aEdgeEnd.getX(), aCurrentSVGPosition.getX(), bUseRelativeCoordinates, bOOXMLMotionPath); + putNumberChar(aResult, aEdgeEnd.getY(), aCurrentSVGPosition.getY(), bUseRelativeCoordinates, bOOXMLMotionPath); + aCurrentSVGPosition = aEdgeEnd; + } + } + } + + // prepare edge start for next loop step + aEdgeStart = aEdgeEnd; + } + + // close path if closed poly (Z and z are equivalent here, but looks nicer when case is matched) + if(aPolygon.isClosed()) + { + putCommandChar(aResult, aLastSVGCommand, 'Z', bUseRelativeCoordinates, bOOXMLMotionPath); + } + else if (bOOXMLMotionPath) + { + putCommandChar(aResult, aLastSVGCommand, 'E', bUseRelativeCoordinates, bOOXMLMotionPath); + } + + if(!bHandleRelativeNextPointCompatible) + { + // SVG defines that "the next subpath starts at the same initial point as the current subpath", + // so set aCurrentSVGPosition to the 1st point of the current, now ended and written path + aCurrentSVGPosition = aPolygon.getB2DPoint(0); + } + } + } + + return aResult.makeStringAndClear(); + } +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/basegfx/source/polygon/b2dtrapezoid.cxx b/basegfx/source/polygon/b2dtrapezoid.cxx new file mode 100644 index 0000000000..2870c46d82 --- /dev/null +++ b/basegfx/source/polygon/b2dtrapezoid.cxx @@ -0,0 +1,1163 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * 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 . + */ + +#include <basegfx/polygon/b2dtrapezoid.hxx> +#include <basegfx/range/b1drange.hxx> +#include <basegfx/polygon/b2dpolygontools.hxx> +#include <basegfx/polygon/b2dpolypolygon.hxx> + +#include <osl/diagnose.h> + +#include <list> + +namespace basegfx::trapezoidhelper +{ + + // helper class to hold a simple edge. This is only used for horizontal edges + // currently, thus the YPositions will be equal. I did not create a special + // class for this since holding the pointers is more effective and also can be + // used as baseclass for the traversing edges + + namespace { + + class TrDeSimpleEdge + { + protected: + // pointers to start and end point + const B2DPoint* mpStart; + const B2DPoint* mpEnd; + + public: + // constructor + TrDeSimpleEdge( + const B2DPoint* pStart, + const B2DPoint* pEnd) + : mpStart(pStart), + mpEnd(pEnd) + { + } + + // data read access + const B2DPoint& getStart() const { return *mpStart; } + const B2DPoint& getEnd() const { return *mpEnd; } + }; + + } + + // define vector of simple edges + + typedef std::vector< TrDeSimpleEdge > TrDeSimpleEdges; + + // helper class for holding a traversing edge. It will always have some + // distance in YPos. The slope (in a numerically useful form, see comments) is + // hold and used in SortValue to allow sorting traversing edges by Y, X and slope + // (in that order) + + namespace { + + class TrDeEdgeEntry : public TrDeSimpleEdge + { + private: + // the slope in a numerical useful form for sorting + sal_uInt32 mnSortValue; + + public: + // convenience data read access + double getDeltaX() const { return mpEnd->getX() - mpStart->getX(); } + double getDeltaY() const { return mpEnd->getY() - mpStart->getY(); } + + // convenience data read access. SortValue is created on demand since + // it is not always used + sal_uInt32 getSortValue() const + { + if(mnSortValue != 0) + return mnSortValue; + + // get radiant; has to be in the range ]0.0 .. pi[, thus scale to full + // sal_uInt32 range for maximum precision + const double fRadiant(atan2(getDeltaY(), getDeltaX()) * (SAL_MAX_UINT32 / M_PI)); + + // convert to sal_uInt32 value + const_cast< TrDeEdgeEntry* >(this)->mnSortValue = sal_uInt32(fRadiant); + + return mnSortValue; + } + + // constructor. SortValue can be given when known, use zero otherwise + TrDeEdgeEntry( + const B2DPoint* pStart, + const B2DPoint* pEnd, + sal_uInt32 nSortValue) + : TrDeSimpleEdge(pStart, pEnd), + mnSortValue(nSortValue) + { + // force traversal of deltaY downward + if(mpEnd->getY() < mpStart->getY()) + { + std::swap(mpStart, mpEnd); + } + + // no horizontal edges allowed, all need to traverse vertically + OSL_ENSURE(mpEnd->getY() > mpStart->getY(), "Illegal TrDeEdgeEntry constructed (!)"); + } + + // data write access to StartPoint + void setStart( const B2DPoint* pNewStart) + { + OSL_ENSURE(pNewStart != nullptr, "No null pointer allowed here (!)"); + + if(mpStart != pNewStart) + { + mpStart = pNewStart; + + // no horizontal edges allowed, all need to traverse vertically + OSL_ENSURE(mpEnd->getY() > mpStart->getY(), "Illegal TrDeEdgeEntry constructed (!)"); + } + } + + // data write access to EndPoint + void setEnd( const B2DPoint* pNewEnd) + { + OSL_ENSURE(pNewEnd != nullptr, "No null pointer allowed here (!)"); + + if(mpEnd != pNewEnd) + { + mpEnd = pNewEnd; + + // no horizontal edges allowed, all need to traverse vertically + OSL_ENSURE(mpEnd->getY() > mpStart->getY(), "Illegal TrDeEdgeEntry constructed (!)"); + } + } + + // operator for sort support. Sort by Y, X and slope (in that order) + bool operator<(const TrDeEdgeEntry& rComp) const + { + if(fTools::equal(getStart().getY(), rComp.getStart().getY())) + { + if(fTools::equal(getStart().getX(), rComp.getStart().getX())) + { + // when start points are equal, use the direction the edge is pointing + // to. That value is created on demand and derived from atan2 in the + // range ]0.0 .. pi[ (without extremas, we always have a deltaY in this + // class) and scaled to sal_uInt32 range for best precision. 0 means no angle, + // while SAL_MAX_UINT32 means pi. Thus, the higher the value, the more left + // the edge traverses. + return (getSortValue() > rComp.getSortValue()); + } + else + { + return fTools::less(getStart().getX(), rComp.getStart().getX()); + } + } + else + { + return fTools::less(getStart().getY(), rComp.getStart().getY()); + } + } + + // method for cut support + B2DPoint getCutPointForGivenY(double fGivenY) const + { + // avoid div/0 + if (getDeltaY() == 0) + return B2DPoint(getStart().getX(), fGivenY); + // Calculate cut point locally (do not use interpolate) since it is numerically + // necessary to guarantee the new, equal Y-coordinate + const double fFactor((fGivenY - getStart().getY()) / getDeltaY()); + const double fDeltaXNew(fFactor * getDeltaX()); + + return B2DPoint(getStart().getX() + fDeltaXNew, fGivenY); + } + }; + + } + + // define double linked list of edges (for fast random insert) + + typedef std::list< TrDeEdgeEntry > TrDeEdgeEntries; + + + + // FIXME: templatize this and use it for TrDeEdgeEntries too ... + + namespace { + + /// Class to allow efficient allocation and release of B2DPoints + class PointBlockAllocator + { + static const size_t nBlockSize = 32; + size_t nCurPoint; + B2DPoint *mpPointBase; + /// Special case the first allocation to avoid it. + B2DPoint maFirstStackBlock[nBlockSize]; + std::vector< B2DPoint * > maBlocks; + public: + PointBlockAllocator() : + nCurPoint( nBlockSize ), + mpPointBase( maFirstStackBlock ) + { + } + + ~PointBlockAllocator() + { + while(!maBlocks.empty()) + { + delete [] maBlocks.back(); + maBlocks.pop_back(); + } + } + + B2DPoint *allocatePoint() + { + if(nCurPoint >= nBlockSize) + { + mpPointBase = new B2DPoint[nBlockSize]; + maBlocks.push_back(mpPointBase); + nCurPoint = 0; + } + return mpPointBase + nCurPoint++; + } + + B2DPoint *allocatePoint(const B2DTuple &rPoint) + { + B2DPoint *pPoint = allocatePoint(); + *pPoint = rPoint; + return pPoint; + } + + /// This is a very uncommon case but why not ... + void freeIfLast(B2DPoint const *pPoint) + { + // just re-use the last point if we can. + if ( nCurPoint > 0 && pPoint == mpPointBase + nCurPoint - 1 ) + nCurPoint--; + } + }; + + // helper class to handle the complete trapezoid subdivision of a PolyPolygon + class TrapezoidSubdivider + { + private: + // local data + TrDeEdgeEntries maTrDeEdgeEntries; + std::vector< B2DPoint > maPoints; + /// new points allocated for cuts + PointBlockAllocator maNewPoints; + + void addEdgeSorted( + TrDeEdgeEntries::iterator aCurrent, + const TrDeEdgeEntry& rNewEdge) + { + // Loop while new entry is bigger, use operator< + while(aCurrent != maTrDeEdgeEntries.end() && (*aCurrent) < rNewEdge) + { + ++aCurrent; + } + + // Insert before first which is smaller or equal or at end + maTrDeEdgeEntries.insert(aCurrent, rNewEdge); + } + + bool splitEdgeAtGivenPoint( + TrDeEdgeEntries::reference aEdge, + const B2DPoint& rCutPoint, + const TrDeEdgeEntries::iterator& aCurrent) + { + // do not create edges without deltaY: do not split when start is identical + if(aEdge.getStart().equal(rCutPoint)) + { + return false; + } + + // do not create edges without deltaY: do not split when end is identical + if(aEdge.getEnd().equal(rCutPoint)) + { + return false; + } + + const double fOldDeltaYStart(rCutPoint.getY() - aEdge.getStart().getY()); + + if(fTools::lessOrEqual(fOldDeltaYStart, 0.0)) + { + // do not split: the resulting edge would be horizontal + // correct it to new start point + aEdge.setStart(&rCutPoint); + return false; + } + + const double fNewDeltaYStart(aEdge.getEnd().getY() - rCutPoint.getY()); + + if(fTools::lessOrEqual(fNewDeltaYStart, 0.0)) + { + // do not split: the resulting edge would be horizontal + // correct it to new end point + aEdge.setEnd(&rCutPoint); + return false; + } + + // Create new entry + const TrDeEdgeEntry aNewEdge( + &rCutPoint, + &aEdge.getEnd(), + aEdge.getSortValue()); + + // Correct old entry + aEdge.setEnd(&rCutPoint); + + // Insert sorted (to avoid new sort) + addEdgeSorted(aCurrent, aNewEdge); + + return true; + } + + bool testAndCorrectEdgeIntersection( + TrDeEdgeEntries::reference aEdgeA, + TrDeEdgeEntries::reference aEdgeB, + const TrDeEdgeEntries::iterator& aCurrent) + { + // Exclude simple cases: same start or end point + if(aEdgeA.getStart().equal(aEdgeB.getStart())) + { + return false; + } + + if(aEdgeA.getStart().equal(aEdgeB.getEnd())) + { + return false; + } + + if(aEdgeA.getEnd().equal(aEdgeB.getStart())) + { + return false; + } + + if(aEdgeA.getEnd().equal(aEdgeB.getEnd())) + { + return false; + } + + // Exclude simple cases: one of the edges has no length anymore + if(aEdgeA.getStart().equal(aEdgeA.getEnd())) + { + return false; + } + + if(aEdgeB.getStart().equal(aEdgeB.getEnd())) + { + return false; + } + + // check if one point is on the other edge (a touch, not a cut) + const B2DVector aDeltaB(aEdgeB.getDeltaX(), aEdgeB.getDeltaY()); + + if(utils::isPointOnEdge(aEdgeA.getStart(), aEdgeB.getStart(), aDeltaB)) + { + return splitEdgeAtGivenPoint(aEdgeB, aEdgeA.getStart(), aCurrent); + } + + if(utils::isPointOnEdge(aEdgeA.getEnd(), aEdgeB.getStart(), aDeltaB)) + { + return splitEdgeAtGivenPoint(aEdgeB, aEdgeA.getEnd(), aCurrent); + } + + const B2DVector aDeltaA(aEdgeA.getDeltaX(), aEdgeA.getDeltaY()); + + if(utils::isPointOnEdge(aEdgeB.getStart(), aEdgeA.getStart(), aDeltaA)) + { + return splitEdgeAtGivenPoint(aEdgeA, aEdgeB.getStart(), aCurrent); + } + + if(utils::isPointOnEdge(aEdgeB.getEnd(), aEdgeA.getStart(), aDeltaA)) + { + return splitEdgeAtGivenPoint(aEdgeA, aEdgeB.getEnd(), aCurrent); + } + + // check for cut inside edges. Use both t-values to choose the more precise + // one later + double fCutA(0.0); + double fCutB(0.0); + + if(utils::findCut( + aEdgeA.getStart(), aDeltaA, + aEdgeB.getStart(), aDeltaB, + CutFlagValue::LINE, + &fCutA, + &fCutB) == CutFlagValue::NONE) + return false; + + // use a simple metric (length criteria) for choosing the numerically + // better cut + const double fSimpleLengthA(aDeltaA.getX() + aDeltaA.getY()); + const double fSimpleLengthB(aDeltaB.getX() + aDeltaB.getY()); + const bool bAIsLonger(fSimpleLengthA > fSimpleLengthB); + B2DPoint* pNewPoint = bAIsLonger + ? maNewPoints.allocatePoint(aEdgeA.getStart() + (fCutA * aDeltaA)) + : maNewPoints.allocatePoint(aEdgeB.getStart() + (fCutB * aDeltaB)); + + // try to split both edges + bool bRetval = splitEdgeAtGivenPoint(aEdgeA, *pNewPoint, aCurrent); + bRetval |= splitEdgeAtGivenPoint(aEdgeB, *pNewPoint, aCurrent); + + if(!bRetval) + maNewPoints.freeIfLast(pNewPoint); + + return bRetval; + } + + void solveHorizontalEdges(TrDeSimpleEdges& rTrDeSimpleEdges) + { + if(rTrDeSimpleEdges.empty() || maTrDeEdgeEntries.empty()) + return; + + // there were horizontal edges. These can be excluded, but + // cuts with other edges need to be solved and added before + // ignoring them + for(const TrDeSimpleEdge & rHorEdge : rTrDeSimpleEdges) + { + // get horizontal edge as candidate; prepare its range and fixed Y + const B1DRange aRange(rHorEdge.getStart().getX(), rHorEdge.getEnd().getX()); + const double fFixedY(rHorEdge.getStart().getY()); + + // loop over traversing edges + TrDeEdgeEntries::iterator aCurrent(maTrDeEdgeEntries.begin()); + + do + { + // get compare edge + TrDeEdgeEntries::reference aCompare(*aCurrent++); + + if(fTools::lessOrEqual(aCompare.getEnd().getY(), fFixedY)) + { + // edge ends above horizontal edge, continue + continue; + } + + if(fTools::moreOrEqual(aCompare.getStart().getY(), fFixedY)) + { + // edge starts below horizontal edge, continue + continue; + } + + // vertical overlap, get horizontal range + const B1DRange aCompareRange(aCompare.getStart().getX(), aCompare.getEnd().getX()); + + if(aRange.overlaps(aCompareRange)) + { + // possible cut, get cut point + const B2DPoint aSplit(aCompare.getCutPointForGivenY(fFixedY)); + + if(fTools::more(aSplit.getX(), aRange.getMinimum()) + && fTools::less(aSplit.getX(), aRange.getMaximum())) + { + // cut is in XRange of horizontal edge, potentially needed cut + B2DPoint* pNewPoint = maNewPoints.allocatePoint(aSplit); + + if(!splitEdgeAtGivenPoint(aCompare, *pNewPoint, aCurrent)) + { + maNewPoints.freeIfLast(pNewPoint); + } + } + } + } + while(aCurrent != maTrDeEdgeEntries.end() + && fTools::less(aCurrent->getStart().getY(), fFixedY)); + } + } + + public: + explicit TrapezoidSubdivider( + const B2DPolyPolygon& rSourcePolyPolygon) + { + B2DPolyPolygon aSource(rSourcePolyPolygon); + TrDeSimpleEdges aTrDeSimpleEdges; + sal_uInt32 nAllPointCount(0); + + // ensure there are no curves used + if(aSource.areControlPointsUsed()) + { + aSource = aSource.getDefaultAdaptiveSubdivision(); + } + + for(const auto& aPolygonCandidate : std::as_const(aSource)) + { + // 1st run: count points + const sal_uInt32 nCount(aPolygonCandidate.count()); + + if(nCount > 2) + { + nAllPointCount += nCount; + } + } + + if(nAllPointCount) + { + // reserve needed points. CAUTION: maPoints size is NOT to be changed anymore + // after 2nd loop since pointers to it are used in the edges + maPoints.reserve(nAllPointCount); + + for(const auto& aPolygonCandidate : std::as_const(aSource)) + { + // 2nd run: add points + const sal_uInt32 nCount(aPolygonCandidate.count()); + + if(nCount > 2) + { + for(sal_uInt32 b = 0; b < nCount; b++) + { + maPoints.push_back(aPolygonCandidate.getB2DPoint(b)); + } + } + } + + // Moved the edge construction to a 3rd run: doing it in the 2nd run is + // possible (and I used it), but requires a working vector::reserve() + // implementation, else the vector will be reallocated and the pointers + // in the edges may be wrong. Security first here. + sal_uInt32 nStartIndex(0); + + for(const auto& aPolygonCandidate : std::as_const(aSource)) + { + const sal_uInt32 nCount(aPolygonCandidate.count()); + + if(nCount > 2) + { + // get the last point of the current polygon + B2DPoint* pPrev(&maPoints[nCount + nStartIndex - 1]); + + for(sal_uInt32 b = 0; b < nCount; b++) + { + // get next point + B2DPoint* pCurr(&maPoints[nStartIndex++]); + + if(fTools::equal(pPrev->getY(), pCurr->getY())) + { + // horizontal edge, check for single point + if(!fTools::equal(pPrev->getX(), pCurr->getX())) + { + // X-order not needed, just add + aTrDeSimpleEdges.emplace_back(pPrev, pCurr); + + const double fMiddle((pPrev->getY() + pCurr->getY()) * 0.5); + pPrev->setY(fMiddle); + pCurr->setY(fMiddle); + } + } + else + { + // vertical edge. Positive Y-direction is guaranteed by the + // TrDeEdgeEntry constructor + maTrDeEdgeEntries.emplace_back(pPrev, pCurr, 0); + } + + // prepare next step + pPrev = pCurr; + } + } + } + } + + if(!maTrDeEdgeEntries.empty()) + { + // single and initial sort of traversing edges + maTrDeEdgeEntries.sort(); + + // solve horizontal edges if there are any detected + solveHorizontalEdges(aTrDeSimpleEdges); + } + } + + void Subdivide(B2DTrapezoidVector& ro_Result) + { + // This is the central subdivider. The strategy is to use the first two entries + // from the traversing edges as a potential trapezoid and do the needed corrections + // and adaptations on the way. + + // There always must be two edges with the same YStart value: When adding the polygons + // in the constructor, there is always a topmost point from which two edges start; when + // the topmost is an edge, there is a start and end of this edge from which two edges + // start. All cases have two edges with same StartY (QED). + + // Based on this these edges get corrected when: + // - one is longer than the other + // - they intersect + // - they intersect with other edges + // - another edge starts inside the thought trapezoid + + // All this cases again produce a valid state so that the first two edges have a common + // Ystart again. Some cases lead to a restart of the process, some allow consuming the + // edges and create the intended trapezoid. + + // Be careful when doing changes here: it is essential to keep all possible paths + // in valid states and to be numerically correct. This is especially needed e.g. + // by using fTools::equal(..) in the more robust small-value incarnation. + B1DRange aLeftRange; + B1DRange aRightRange; + + if(!maTrDeEdgeEntries.empty()) + { + // measuring shows that the relation between edges and created trapezoids is + // mostly in the 1:1 range, thus reserve as much trapezoids as edges exist. + ro_Result.reserve(ro_Result.size() + maTrDeEdgeEntries.size()); + } + + while(!maTrDeEdgeEntries.empty()) + { + // Prepare current operator and get first edge + TrDeEdgeEntries::iterator aCurrent(maTrDeEdgeEntries.begin()); + TrDeEdgeEntries::reference aLeft(*aCurrent++); + + if(aCurrent == maTrDeEdgeEntries.end()) + { + // Should not happen: No 2nd edge; consume the single edge + // to not have an endless loop and start next. During development + // I constantly had breakpoints here, so I am sure enough to add an + // assertion here + OSL_FAIL("Trapezoid decomposer in illegal state (!)"); + maTrDeEdgeEntries.pop_front(); + continue; + } + + // get second edge + TrDeEdgeEntries::reference aRight(*aCurrent++); + + if(!fTools::equal(aLeft.getStart().getY(), aRight.getStart().getY())) + { + // Should not happen: We have a 2nd edge, but YStart is on another + // line; consume the single edge to not have an endless loop and start + // next. During development I constantly had breakpoints here, so I am + // sure enough to add an assertion here + OSL_FAIL("Trapezoid decomposer in illegal state (!)"); + maTrDeEdgeEntries.pop_front(); + continue; + } + + // aLeft and aRight build a thought trapezoid now. They have a common + // start line (same Y for start points). Potentially, one of the edges + // is longer than the other. It is only needed to look at the shorter + // length which build the potential trapezoid. To do so, get the end points + // locally and adapt the evtl. longer one. Use only aLeftEnd and aRightEnd + // from here on, not the aLeft.getEnd() or aRight.getEnd() accesses. + B2DPoint aLeftEnd(aLeft.getEnd()); + B2DPoint aRightEnd(aRight.getEnd()); + + // check if end points are on the same line. If yes, no adaptation + // needs to be prepared. Also remember which one actually is longer. + const bool bEndOnSameLine(fTools::equal(aLeftEnd.getY(), aRightEnd.getY())); + bool bLeftIsLonger(false); + + if(!bEndOnSameLine) + { + // check which edge is longer and correct accordingly + bLeftIsLonger = fTools::more(aLeftEnd.getY(), aRightEnd.getY()); + + if(bLeftIsLonger) + { + aLeftEnd = aLeft.getCutPointForGivenY(aRightEnd.getY()); + } + else + { + aRightEnd = aRight.getCutPointForGivenY(aLeftEnd.getY()); + } + } + + // check for same start and end points + const bool bSameStartPoint(aLeft.getStart().equal(aRight.getStart())); + const bool bSameEndPoint(aLeftEnd.equal(aRightEnd)); + + // check the simple case that the edges form a 'blind' edge (deadend) + if(bSameStartPoint && bSameEndPoint) + { + // correct the longer edge if prepared + if(!bEndOnSameLine) + { + if(bLeftIsLonger) + { + B2DPoint* pNewPoint = maNewPoints.allocatePoint(aLeftEnd); + + if(!splitEdgeAtGivenPoint(aLeft, *pNewPoint, aCurrent)) + { + maNewPoints.freeIfLast(pNewPoint); + } + } + else + { + B2DPoint* pNewPoint = maNewPoints.allocatePoint(aRightEnd); + + if(!splitEdgeAtGivenPoint(aRight, *pNewPoint, aCurrent)) + { + maNewPoints.freeIfLast(pNewPoint); + } + } + } + + // consume both edges and start next run + maTrDeEdgeEntries.pop_front(); + maTrDeEdgeEntries.pop_front(); + + continue; + } + + // check if the edges self-intersect. This can only happen when + // start and end point are different + bool bRangesSet(false); + + if(!(bSameStartPoint || bSameEndPoint)) + { + // get XRanges of edges + aLeftRange = B1DRange(aLeft.getStart().getX(), aLeftEnd.getX()); + aRightRange = B1DRange(aRight.getStart().getX(), aRightEnd.getX()); + bRangesSet = true; + + // use fast range test first + if(aLeftRange.overlaps(aRightRange)) + { + // real cut test and correction. If correction was needed, + // start new run + if(testAndCorrectEdgeIntersection(aLeft, aRight, aCurrent)) + { + continue; + } + } + } + + // now we need to check if there are intersections with other edges + // or if other edges start inside the candidate trapezoid + if(aCurrent != maTrDeEdgeEntries.end() + && fTools::less(aCurrent->getStart().getY(), aLeftEnd.getY())) + { + // get XRanges of edges + if(!bRangesSet) + { + aLeftRange = B1DRange(aLeft.getStart().getX(), aLeftEnd.getX()); + aRightRange = B1DRange(aRight.getStart().getX(), aRightEnd.getX()); + } + + // build full XRange for fast check + B1DRange aAllRange(aLeftRange); + aAllRange.expand(aRightRange); + + // prepare loop iterator; aCurrent needs to stay unchanged for + // possibly sorted insertions of new EdgeNodes. Also prepare stop flag + TrDeEdgeEntries::iterator aLoop(aCurrent); + bool bDone(false); + + do + { + // get compare edge and its XRange + TrDeEdgeEntries::reference aCompare(*aLoop++); + + // avoid edges using the same start point as one of + // the edges. These can neither have their start point + // in the thought trapezoid nor cut with one of the edges + if(aCompare.getStart().equal(aRight.getStart())) + { + continue; + } + + // get compare XRange + const B1DRange aCompareRange(aCompare.getStart().getX(), aCompare.getEnd().getX()); + + // use fast range test first + if(aAllRange.overlaps(aCompareRange)) + { + // check for start point inside thought trapezoid + if(fTools::more(aCompare.getStart().getY(), aLeft.getStart().getY())) + { + // calculate the two possible split points at compare's Y + const B2DPoint aSplitLeft(aLeft.getCutPointForGivenY(aCompare.getStart().getY())); + const B2DPoint aSplitRight(aRight.getCutPointForGivenY(aCompare.getStart().getY())); + + // check for start point of aCompare being inside thought + // trapezoid + if(aCompare.getStart().getX() >= aSplitLeft.getX() && + aCompare.getStart().getX() <= aSplitRight.getX()) + { + // is inside, correct and restart loop + B2DPoint* pNewLeft = maNewPoints.allocatePoint(aSplitLeft); + + if(splitEdgeAtGivenPoint(aLeft, *pNewLeft, aCurrent)) + { + bDone = true; + } + else + { + maNewPoints.freeIfLast(pNewLeft); + } + + B2DPoint* pNewRight = maNewPoints.allocatePoint(aSplitRight); + + if(splitEdgeAtGivenPoint(aRight, *pNewRight, aCurrent)) + { + bDone = true; + } + else + { + maNewPoints.freeIfLast(pNewRight); + } + } + } + + if(!bDone && aLeftRange.overlaps(aCompareRange)) + { + // test for concrete cut of compare edge with left edge + bDone = testAndCorrectEdgeIntersection(aLeft, aCompare, aCurrent); + } + + if(!bDone && aRightRange.overlaps(aCompareRange)) + { + // test for concrete cut of compare edge with Right edge + bDone = testAndCorrectEdgeIntersection(aRight, aCompare, aCurrent); + } + } + } + while(!bDone + && aLoop != maTrDeEdgeEntries.end() + && fTools::less(aLoop->getStart().getY(), aLeftEnd.getY())); + + if(bDone) + { + // something needed to be changed; start next loop + continue; + } + } + + // when we get here, the intended trapezoid can be used. It needs to + // be corrected possibly (if prepared); but this is no reason not to + // use it in the same loop iteration + if(!bEndOnSameLine) + { + if(bLeftIsLonger) + { + B2DPoint* pNewPoint = maNewPoints.allocatePoint(aLeftEnd); + + if(!splitEdgeAtGivenPoint(aLeft, *pNewPoint, aCurrent)) + { + maNewPoints.freeIfLast(pNewPoint); + } + } + else + { + B2DPoint* pNewPoint = maNewPoints.allocatePoint(aRightEnd); + + if(!splitEdgeAtGivenPoint(aRight, *pNewPoint, aCurrent)) + { + maNewPoints.freeIfLast(pNewPoint); + } + } + } + + // the two edges start at the same Y, they use the same DeltaY, they + // do not cut themselves and not any other edge in range. Create a + // B2DTrapezoid and consume both edges + ro_Result.emplace_back( + aLeft.getStart().getX(), + aRight.getStart().getX(), + aLeft.getStart().getY(), + aLeftEnd.getX(), + aRightEnd.getX(), + aLeftEnd.getY()); + + maTrDeEdgeEntries.pop_front(); + maTrDeEdgeEntries.pop_front(); + } + } + }; + + } +} // end of namespace + +namespace basegfx +{ + B2DTrapezoid::B2DTrapezoid( + const double& rfTopXLeft, + const double& rfTopXRight, + const double& rfTopY, + const double& rfBottomXLeft, + const double& rfBottomXRight, + const double& rfBottomY) + : mfTopXLeft(rfTopXLeft), + mfTopXRight(rfTopXRight), + mfTopY(rfTopY), + mfBottomXLeft(rfBottomXLeft), + mfBottomXRight(rfBottomXRight), + mfBottomY(rfBottomY) + { + // guarantee mfTopXRight >= mfTopXLeft + if(mfTopXLeft > mfTopXRight) + { + std::swap(mfTopXLeft, mfTopXRight); + } + + // guarantee mfBottomXRight >= mfBottomXLeft + if(mfBottomXLeft > mfBottomXRight) + { + std::swap(mfBottomXLeft, mfBottomXRight); + } + + // guarantee mfBottomY >= mfTopY + if(mfTopY > mfBottomY) + { + std::swap(mfTopY, mfBottomY); + std::swap(mfTopXLeft, mfBottomXLeft); + std::swap(mfTopXRight, mfBottomXRight); + } + } + + B2DPolygon B2DTrapezoid::getB2DPolygon() const + { + B2DPolygon aRetval; + + aRetval.append(B2DPoint(getTopXLeft(), getTopY())); + aRetval.append(B2DPoint(getTopXRight(), getTopY())); + aRetval.append(B2DPoint(getBottomXRight(), getBottomY())); + aRetval.append(B2DPoint(getBottomXLeft(), getBottomY())); + aRetval.setClosed(true); + + return aRetval; + } +} // end of namespace basegfx + +namespace basegfx::utils +{ + // convert Source utils::PolyPolygon to trapezoids + void trapezoidSubdivide(B2DTrapezoidVector& ro_Result, const B2DPolyPolygon& rSourcePolyPolygon) + { + trapezoidhelper::TrapezoidSubdivider aTrapezoidSubdivider(rSourcePolyPolygon); + + aTrapezoidSubdivider.Subdivide(ro_Result); + } + + void createLineTrapezoidFromEdge( + B2DTrapezoidVector& ro_Result, + const B2DPoint& rPointA, + const B2DPoint& rPointB, + double fLineWidth) + { + if(fTools::lessOrEqual(fLineWidth, 0.0)) + { + // no line width + return; + } + + if(rPointA.equal(rPointB)) + { + // points are equal, no edge + return; + } + + const double fHalfLineWidth(0.5 * fLineWidth); + + if(fTools::equal(rPointA.getX(), rPointB.getX())) + { + // vertical line + const double fLeftX(rPointA.getX() - fHalfLineWidth); + const double fRightX(rPointA.getX() + fHalfLineWidth); + + ro_Result.emplace_back( + fLeftX, + fRightX, + std::min(rPointA.getY(), rPointB.getY()), + fLeftX, + fRightX, + std::max(rPointA.getY(), rPointB.getY())); + } + else if(fTools::equal(rPointA.getY(), rPointB.getY())) + { + // horizontal line + const double fLeftX(std::min(rPointA.getX(), rPointB.getX())); + const double fRightX(std::max(rPointA.getX(), rPointB.getX())); + + ro_Result.emplace_back( + fLeftX, + fRightX, + rPointA.getY() - fHalfLineWidth, + fLeftX, + fRightX, + rPointA.getY() + fHalfLineWidth); + } + else + { + // diagonal line + // create perpendicular vector + const B2DVector aDelta(rPointB - rPointA); + B2DVector aPerpendicular(-aDelta.getY(), aDelta.getX()); + aPerpendicular.setLength(fHalfLineWidth); + + // create StartLow, StartHigh, EndLow and EndHigh + const B2DPoint aStartLow(rPointA + aPerpendicular); + const B2DPoint aStartHigh(rPointA - aPerpendicular); + const B2DPoint aEndHigh(rPointB - aPerpendicular); + const B2DPoint aEndLow(rPointB + aPerpendicular); + + // create EdgeEntries + basegfx::trapezoidhelper::TrDeEdgeEntries aTrDeEdgeEntries; + + aTrDeEdgeEntries.emplace_back(&aStartLow, &aStartHigh, 0); + aTrDeEdgeEntries.emplace_back(&aStartHigh, &aEndHigh, 0); + aTrDeEdgeEntries.emplace_back(&aEndHigh, &aEndLow, 0); + aTrDeEdgeEntries.emplace_back(&aEndLow, &aStartLow, 0); + aTrDeEdgeEntries.sort(); + + // here we know we have exactly four edges, and they do not cut, touch or + // intersect. This makes processing much easier. Get the first two as start + // edges for the thought trapezoid + basegfx::trapezoidhelper::TrDeEdgeEntries::iterator aCurrent(aTrDeEdgeEntries.begin()); + basegfx::trapezoidhelper::TrDeEdgeEntries::reference aLeft(*aCurrent++); + basegfx::trapezoidhelper::TrDeEdgeEntries::reference aRight(*aCurrent++); + const bool bEndOnSameLine(fTools::equal(aLeft.getEnd().getY(), aRight.getEnd().getY())); + + if(bEndOnSameLine) + { + // create two triangle trapezoids + ro_Result.emplace_back( + aLeft.getStart().getX(), + aRight.getStart().getX(), + aLeft.getStart().getY(), + aLeft.getEnd().getX(), + aRight.getEnd().getX(), + aLeft.getEnd().getY()); + + basegfx::trapezoidhelper::TrDeEdgeEntries::reference aLeft2(*aCurrent++); + basegfx::trapezoidhelper::TrDeEdgeEntries::reference aRight2(*aCurrent++); + + ro_Result.emplace_back( + aLeft2.getStart().getX(), + aRight2.getStart().getX(), + aLeft2.getStart().getY(), + aLeft2.getEnd().getX(), + aRight2.getEnd().getX(), + aLeft2.getEnd().getY()); + } + else + { + // create three trapezoids. Check which edge is longer and + // correct accordingly + const bool bLeftIsLonger(fTools::more(aLeft.getEnd().getY(), aRight.getEnd().getY())); + + if(bLeftIsLonger) + { + basegfx::trapezoidhelper::TrDeEdgeEntries::reference aRight2(*aCurrent++); + basegfx::trapezoidhelper::TrDeEdgeEntries::reference aLeft2(*aCurrent++); + const B2DPoint aSplitLeft(aLeft.getCutPointForGivenY(aRight.getEnd().getY())); + const B2DPoint aSplitRight(aRight2.getCutPointForGivenY(aLeft.getEnd().getY())); + + ro_Result.emplace_back( + aLeft.getStart().getX(), + aRight.getStart().getX(), + aLeft.getStart().getY(), + aSplitLeft.getX(), + aRight.getEnd().getX(), + aRight.getEnd().getY()); + + ro_Result.emplace_back( + aSplitLeft.getX(), + aRight.getEnd().getX(), + aRight.getEnd().getY(), + aLeft2.getStart().getX(), + aSplitRight.getX(), + aLeft2.getStart().getY()); + + ro_Result.emplace_back( + aLeft2.getStart().getX(), + aSplitRight.getX(), + aLeft2.getStart().getY(), + aLeft2.getEnd().getX(), + aRight2.getEnd().getX(), + aLeft2.getEnd().getY()); + } + else + { + basegfx::trapezoidhelper::TrDeEdgeEntries::reference aLeft2(*aCurrent++); + basegfx::trapezoidhelper::TrDeEdgeEntries::reference aRight2(*aCurrent++); + const B2DPoint aSplitRight(aRight.getCutPointForGivenY(aLeft.getEnd().getY())); + const B2DPoint aSplitLeft(aLeft2.getCutPointForGivenY(aRight.getEnd().getY())); + + ro_Result.emplace_back( + aLeft.getStart().getX(), + aRight.getStart().getX(), + aLeft.getStart().getY(), + aLeft.getEnd().getX(), + aSplitRight.getX(), + aLeft.getEnd().getY()); + + ro_Result.emplace_back( + aLeft.getEnd().getX(), + aSplitRight.getX(), + aLeft.getEnd().getY(), + aSplitLeft.getX(), + aRight.getEnd().getX(), + aRight2.getStart().getY()); + + ro_Result.emplace_back( + aSplitLeft.getX(), + aRight.getEnd().getX(), + aRight2.getStart().getY(), + aLeft2.getEnd().getX(), + aRight2.getEnd().getX(), + aLeft2.getEnd().getY()); + } + } + } + } + + void createLineTrapezoidFromB2DPolygon( + B2DTrapezoidVector& ro_Result, + const B2DPolygon& rPolygon, + double fLineWidth) + { + if(fTools::lessOrEqual(fLineWidth, 0.0)) + { + return; + } + + // ensure there are no curves used + B2DPolygon aSource(rPolygon); + + if(aSource.areControlPointsUsed()) + { + const double fPrecisionFactor = 0.25; + aSource = adaptiveSubdivideByDistance( aSource, fLineWidth * fPrecisionFactor ); + } + + const sal_uInt32 nPointCount(aSource.count()); + + if(!nPointCount) + { + return; + } + + const sal_uInt32 nEdgeCount(aSource.isClosed() ? nPointCount : nPointCount - 1); + B2DPoint aCurrent(aSource.getB2DPoint(0)); + + ro_Result.reserve(ro_Result.size() + (3 * nEdgeCount)); + + for(sal_uInt32 a(0); a < nEdgeCount; a++) + { + const sal_uInt32 nNextIndex((a + 1) % nPointCount); + const B2DPoint aNext(aSource.getB2DPoint(nNextIndex)); + + createLineTrapezoidFromEdge(ro_Result, aCurrent, aNext, fLineWidth); + aCurrent = aNext; + } + } + + +} // end of namespace + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/basegfx/source/polygon/b3dpolygon.cxx b/basegfx/source/polygon/b3dpolygon.cxx new file mode 100644 index 0000000000..ebd9e3f4f7 --- /dev/null +++ b/basegfx/source/polygon/b3dpolygon.cxx @@ -0,0 +1,1612 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * 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 . + */ + +#include <osl/diagnose.h> +#include <basegfx/polygon/b3dpolygon.hxx> +#include <basegfx/point/b3dpoint.hxx> +#include <basegfx/matrix/b3dhommatrix.hxx> +#include <basegfx/point/b2dpoint.hxx> +#include <basegfx/color/bcolor.hxx> +#include <basegfx/matrix/b2dhommatrix.hxx> +#include <cassert> +#include <memory> +#include <utility> +#include <vector> +#include <algorithm> + +namespace { + +class CoordinateData3D +{ + basegfx::B3DPoint maPoint; + +public: + CoordinateData3D() + { + } + + explicit CoordinateData3D(const basegfx::B3DPoint& rData) + : maPoint(rData) + { + } + + const basegfx::B3DPoint& getCoordinate() const + { + return maPoint; + } + + void setCoordinate(const basegfx::B3DPoint& rValue) + { + if(rValue != maPoint) + maPoint = rValue; + } + + bool operator==(const CoordinateData3D& rData) const + { + return (maPoint == rData.getCoordinate()); + } + + void transform(const basegfx::B3DHomMatrix& rMatrix) + { + maPoint *= rMatrix; + } +}; + +class CoordinateDataArray3D +{ + typedef std::vector< CoordinateData3D > CoordinateData3DVector; + + CoordinateData3DVector maVector; + +public: + explicit CoordinateDataArray3D(sal_uInt32 nCount) + : maVector(nCount) + { + } + + CoordinateDataArray3D(const CoordinateDataArray3D& rOriginal, sal_uInt32 nIndex, sal_uInt32 nCount) + : maVector(rOriginal.maVector.begin() + nIndex, rOriginal.maVector.begin() + (nIndex + nCount)) + { + } + + ::basegfx::B3DVector getNormal() const + { + ::basegfx::B3DVector aRetval; + const sal_uInt32 nPointCount(maVector.size()); + + if(nPointCount > 2) + { + sal_uInt32 nISmallest(0); + sal_uInt32 a(0); + const basegfx::B3DPoint* pSmallest(&maVector[0].getCoordinate()); + const basegfx::B3DPoint* pNext(nullptr); + const basegfx::B3DPoint* pPrev(nullptr); + + // To guarantee a correctly oriented point, choose an outmost one + // which then cannot be concave + for(a = 1; a < nPointCount; a++) + { + const basegfx::B3DPoint& rCandidate = maVector[a].getCoordinate(); + + if((rCandidate.getX() < pSmallest->getX()) + || (rCandidate.getX() == pSmallest->getX() && rCandidate.getY() < pSmallest->getY()) + || (rCandidate.getX() == pSmallest->getX() && rCandidate.getY() == pSmallest->getY() && rCandidate.getZ() < pSmallest->getZ())) + { + nISmallest = a; + pSmallest = &rCandidate; + } + } + + // look for a next point different from minimal one + for(a = (nISmallest + 1) % nPointCount; a != nISmallest; a = (a + 1) % nPointCount) + { + const basegfx::B3DPoint& rCandidate = maVector[a].getCoordinate(); + + if(!rCandidate.equal(*pSmallest)) + { + pNext = &rCandidate; + break; + } + } + + // look for a previous point different from minimal one + for(a = (nISmallest + nPointCount - 1) % nPointCount; a != nISmallest; a = (a + nPointCount - 1) % nPointCount) + { + const basegfx::B3DPoint& rCandidate = maVector[a].getCoordinate(); + + if(!rCandidate.equal(*pSmallest)) + { + pPrev = &rCandidate; + break; + } + } + + // we always have a minimal point. If we also have a different next and previous, + // we can calculate the normal + if(pNext && pPrev) + { + const basegfx::B3DVector aPrev(*pPrev - *pSmallest); + const basegfx::B3DVector aNext(*pNext - *pSmallest); + + aRetval = cross(aPrev, aNext); + aRetval.normalize(); + } + } + + return aRetval; + } + + sal_uInt32 count() const + { + return maVector.size(); + } + + bool operator==(const CoordinateDataArray3D& rCandidate) const + { + return (maVector == rCandidate.maVector); + } + + const basegfx::B3DPoint& getCoordinate(sal_uInt32 nIndex) const + { + return maVector[nIndex].getCoordinate(); + } + + void setCoordinate(sal_uInt32 nIndex, const basegfx::B3DPoint& rValue) + { + maVector[nIndex].setCoordinate(rValue); + } + + void insert(sal_uInt32 nIndex, const CoordinateData3D& rValue, sal_uInt32 nCount) + { + if(nCount) + { + // add nCount copies of rValue + CoordinateData3DVector::iterator aIndex(maVector.begin()); + aIndex += nIndex; + maVector.insert(aIndex, nCount, rValue); + } + } + + void insert(sal_uInt32 nIndex, const CoordinateDataArray3D& rSource) + { + const sal_uInt32 nCount(rSource.maVector.size()); + + if(nCount) + { + // insert data + CoordinateData3DVector::iterator aIndex(maVector.begin()); + aIndex += nIndex; + CoordinateData3DVector::const_iterator aStart(rSource.maVector.begin()); + CoordinateData3DVector::const_iterator aEnd(rSource.maVector.end()); + maVector.insert(aIndex, aStart, aEnd); + } + } + + void remove(sal_uInt32 nIndex, sal_uInt32 nCount) + { + if(nCount) + { + // remove point data + CoordinateData3DVector::iterator aStart(maVector.begin()); + aStart += nIndex; + const CoordinateData3DVector::iterator aEnd(aStart + nCount); + maVector.erase(aStart, aEnd); + } + } + + void flip() + { + if(maVector.size() <= 1) + return; + + const sal_uInt32 nHalfSize(maVector.size() >> 1); + CoordinateData3DVector::iterator aStart(maVector.begin()); + CoordinateData3DVector::iterator aEnd(maVector.end() - 1); + + for(sal_uInt32 a(0); a < nHalfSize; a++) + { + std::swap(*aStart, *aEnd); + ++aStart; + --aEnd; + } + } + + void transform(const ::basegfx::B3DHomMatrix& rMatrix) + { + for (auto & elem : maVector) + { + elem.transform(rMatrix); + } + } +}; + +class BColorArray +{ + typedef std::vector< ::basegfx::BColor > BColorDataVector; + + BColorDataVector maVector; + sal_uInt32 mnUsedEntries; + +public: + explicit BColorArray(sal_uInt32 nCount) + : maVector(nCount), + mnUsedEntries(0) + { + } + + BColorArray(const BColorArray& rOriginal, sal_uInt32 nIndex, sal_uInt32 nCount) + : mnUsedEntries(0) + { + BColorDataVector::const_iterator aStart(rOriginal.maVector.begin()); + aStart += nIndex; + BColorDataVector::const_iterator aEnd(aStart); + assert(nCount <= rOriginal.maVector.size()); + aEnd += nCount; + maVector.reserve(nCount); + + for(; aStart != aEnd; ++aStart) + { + if(!aStart->equalZero()) + mnUsedEntries++; + + maVector.push_back(*aStart); + } + } + + bool operator==(const BColorArray& rCandidate) const + { + return (maVector == rCandidate.maVector); + } + + bool isUsed() const + { + return (mnUsedEntries != 0); + } + + const ::basegfx::BColor& getBColor(sal_uInt32 nIndex) const + { + return maVector[nIndex]; + } + + void setBColor(sal_uInt32 nIndex, const ::basegfx::BColor& rValue) + { + bool bWasUsed(mnUsedEntries && !maVector[nIndex].equalZero()); + bool bIsUsed(!rValue.equalZero()); + + if(bWasUsed) + { + if(bIsUsed) + { + maVector[nIndex] = rValue; + } + else + { + maVector[nIndex] = ::basegfx::BColor::getEmptyBColor(); + mnUsedEntries--; + } + } + else + { + if(bIsUsed) + { + maVector[nIndex] = rValue; + mnUsedEntries++; + } + } + } + + void insert(sal_uInt32 nIndex, const ::basegfx::BColor& rValue, sal_uInt32 nCount) + { + if(nCount) + { + // add nCount copies of rValue + BColorDataVector::iterator aIndex(maVector.begin()); + aIndex += nIndex; + maVector.insert(aIndex, nCount, rValue); + + if(!rValue.equalZero()) + mnUsedEntries += nCount; + } + } + + void insert(sal_uInt32 nIndex, const BColorArray& rSource) + { + const sal_uInt32 nCount(rSource.maVector.size()); + + if(nCount) + { + // insert data + BColorDataVector::iterator aIndex(maVector.begin()); + aIndex += nIndex; + BColorDataVector::const_iterator aStart(rSource.maVector.begin()); + BColorDataVector::const_iterator aEnd(rSource.maVector.end()); + maVector.insert(aIndex, aStart, aEnd); + + mnUsedEntries += std::count_if(aStart, aEnd, + [](BColorDataVector::const_reference rData) { return !rData.equalZero(); }); + } + } + + void remove(sal_uInt32 nIndex, sal_uInt32 nCount) + { + if(nCount) + { + const BColorDataVector::iterator aDeleteStart(maVector.begin() + nIndex); + const BColorDataVector::iterator aDeleteEnd(aDeleteStart + nCount); + + auto nDeleteUsed = std::count_if(aDeleteStart, aDeleteEnd, + [](BColorDataVector::const_reference rData) { return !rData.equalZero(); }); + mnUsedEntries -= std::min(mnUsedEntries, static_cast<sal_uInt32>(nDeleteUsed)); + + // remove point data + maVector.erase(aDeleteStart, aDeleteEnd); + } + } + + void flip() + { + if(maVector.size() <= 1) + return; + + const sal_uInt32 nHalfSize(maVector.size() >> 1); + BColorDataVector::iterator aStart(maVector.begin()); + BColorDataVector::iterator aEnd(maVector.end() - 1); + + for(sal_uInt32 a(0); a < nHalfSize; a++) + { + std::swap(*aStart, *aEnd); + ++aStart; + --aEnd; + } + } +}; + +class NormalsArray3D +{ + typedef std::vector< ::basegfx::B3DVector > NormalsData3DVector; + + NormalsData3DVector maVector; + sal_uInt32 mnUsedEntries; + +public: + explicit NormalsArray3D(sal_uInt32 nCount) + : maVector(nCount), + mnUsedEntries(0) + { + } + + NormalsArray3D(const NormalsArray3D& rOriginal, sal_uInt32 nIndex, sal_uInt32 nCount) + : mnUsedEntries(0) + { + NormalsData3DVector::const_iterator aStart(rOriginal.maVector.begin()); + aStart += nIndex; + NormalsData3DVector::const_iterator aEnd(aStart); + aEnd += nCount; + maVector.reserve(nCount); + + for(; aStart != aEnd; ++aStart) + { + if(!aStart->equalZero()) + mnUsedEntries++; + + maVector.push_back(*aStart); + } + } + + bool operator==(const NormalsArray3D& rCandidate) const + { + return (maVector == rCandidate.maVector); + } + + bool isUsed() const + { + return (mnUsedEntries != 0); + } + + const ::basegfx::B3DVector& getNormal(sal_uInt32 nIndex) const + { + return maVector[nIndex]; + } + + void setNormal(sal_uInt32 nIndex, const ::basegfx::B3DVector& rValue) + { + bool bWasUsed(mnUsedEntries && !maVector[nIndex].equalZero()); + bool bIsUsed(!rValue.equalZero()); + + if(bWasUsed) + { + if(bIsUsed) + { + maVector[nIndex] = rValue; + } + else + { + maVector[nIndex] = ::basegfx::B3DVector::getEmptyVector(); + mnUsedEntries--; + } + } + else + { + if(bIsUsed) + { + maVector[nIndex] = rValue; + mnUsedEntries++; + } + } + } + + void insert(sal_uInt32 nIndex, const ::basegfx::B3DVector& rValue, sal_uInt32 nCount) + { + if(nCount) + { + // add nCount copies of rValue + NormalsData3DVector::iterator aIndex(maVector.begin()); + aIndex += nIndex; + maVector.insert(aIndex, nCount, rValue); + + if(!rValue.equalZero()) + mnUsedEntries += nCount; + } + } + + void insert(sal_uInt32 nIndex, const NormalsArray3D& rSource) + { + const sal_uInt32 nCount(rSource.maVector.size()); + + if(nCount) + { + // insert data + NormalsData3DVector::iterator aIndex(maVector.begin()); + aIndex += nIndex; + NormalsData3DVector::const_iterator aStart(rSource.maVector.begin()); + NormalsData3DVector::const_iterator aEnd(rSource.maVector.end()); + maVector.insert(aIndex, aStart, aEnd); + + mnUsedEntries += std::count_if(aStart, aEnd, + [](NormalsData3DVector::const_reference rData) { return !rData.equalZero(); }); + } + } + + void remove(sal_uInt32 nIndex, sal_uInt32 nCount) + { + if(nCount) + { + const NormalsData3DVector::iterator aDeleteStart(maVector.begin() + nIndex); + const NormalsData3DVector::iterator aDeleteEnd(aDeleteStart + nCount); + + auto nDeleteUsed = std::count_if(aDeleteStart, aDeleteEnd, + [](NormalsData3DVector::const_reference rData) { return !rData.equalZero(); }); + mnUsedEntries -= std::min(mnUsedEntries, static_cast<sal_uInt32>(nDeleteUsed)); + + // remove point data + maVector.erase(aDeleteStart, aDeleteEnd); + } + } + + void flip() + { + if(maVector.size() <= 1) + return; + + const sal_uInt32 nHalfSize(maVector.size() >> 1); + NormalsData3DVector::iterator aStart(maVector.begin()); + NormalsData3DVector::iterator aEnd(maVector.end() - 1); + + for(sal_uInt32 a(0); a < nHalfSize; a++) + { + std::swap(*aStart, *aEnd); + ++aStart; + --aEnd; + } + } + + void transform(const basegfx::B3DHomMatrix& rMatrix) + { + for (auto & elem : maVector) + { + elem *= rMatrix; + } + } +}; + +class TextureCoordinate2D +{ + typedef std::vector< ::basegfx::B2DPoint > TextureData2DVector; + + TextureData2DVector maVector; + sal_uInt32 mnUsedEntries; + +public: + explicit TextureCoordinate2D(sal_uInt32 nCount) + : maVector(nCount), + mnUsedEntries(0) + { + } + + TextureCoordinate2D(const TextureCoordinate2D& rOriginal, sal_uInt32 nIndex, sal_uInt32 nCount) + : mnUsedEntries(0) + { + TextureData2DVector::const_iterator aStart(rOriginal.maVector.begin()); + aStart += nIndex; + TextureData2DVector::const_iterator aEnd(aStart); + aEnd += nCount; + maVector.reserve(nCount); + + for(; aStart != aEnd; ++aStart) + { + if(!aStart->equalZero()) + mnUsedEntries++; + + maVector.push_back(*aStart); + } + } + + bool operator==(const TextureCoordinate2D& rCandidate) const + { + return (maVector == rCandidate.maVector); + } + + bool isUsed() const + { + return (mnUsedEntries != 0); + } + + const ::basegfx::B2DPoint& getTextureCoordinate(sal_uInt32 nIndex) const + { + return maVector[nIndex]; + } + + void setTextureCoordinate(sal_uInt32 nIndex, const ::basegfx::B2DPoint& rValue) + { + bool bWasUsed(mnUsedEntries && !maVector[nIndex].equalZero()); + bool bIsUsed(!rValue.equalZero()); + + if(bWasUsed) + { + if(bIsUsed) + { + maVector[nIndex] = rValue; + } + else + { + maVector[nIndex] = ::basegfx::B2DPoint::getEmptyPoint(); + mnUsedEntries--; + } + } + else + { + if(bIsUsed) + { + maVector[nIndex] = rValue; + mnUsedEntries++; + } + } + } + + void insert(sal_uInt32 nIndex, const ::basegfx::B2DPoint& rValue, sal_uInt32 nCount) + { + if(nCount) + { + // add nCount copies of rValue + TextureData2DVector::iterator aIndex(maVector.begin()); + aIndex += nIndex; + maVector.insert(aIndex, nCount, rValue); + + if(!rValue.equalZero()) + mnUsedEntries += nCount; + } + } + + void insert(sal_uInt32 nIndex, const TextureCoordinate2D& rSource) + { + const sal_uInt32 nCount(rSource.maVector.size()); + + if(nCount) + { + // insert data + TextureData2DVector::iterator aIndex(maVector.begin()); + aIndex += nIndex; + TextureData2DVector::const_iterator aStart(rSource.maVector.begin()); + TextureData2DVector::const_iterator aEnd(rSource.maVector.end()); + maVector.insert(aIndex, aStart, aEnd); + + mnUsedEntries += std::count_if(aStart, aEnd, + [](TextureData2DVector::const_reference rData) { return !rData.equalZero(); }); + } + } + + void remove(sal_uInt32 nIndex, sal_uInt32 nCount) + { + if(nCount) + { + const TextureData2DVector::iterator aDeleteStart(maVector.begin() + nIndex); + const TextureData2DVector::iterator aDeleteEnd(aDeleteStart + nCount); + + auto nDeleteUsed = std::count_if(aDeleteStart, aDeleteEnd, + [](TextureData2DVector::const_reference rData) { return !rData.equalZero(); }); + mnUsedEntries -= std::min(mnUsedEntries, static_cast<sal_uInt32>(nDeleteUsed)); + + // remove point data + maVector.erase(aDeleteStart, aDeleteEnd); + } + } + + void flip() + { + if(maVector.size() <= 1) + return; + + const sal_uInt32 nHalfSize(maVector.size() >> 1); + TextureData2DVector::iterator aStart(maVector.begin()); + TextureData2DVector::iterator aEnd(maVector.end() - 1); + + for(sal_uInt32 a(0); a < nHalfSize; a++) + { + std::swap(*aStart, *aEnd); + ++aStart; + --aEnd; + } + } + + void transform(const ::basegfx::B2DHomMatrix& rMatrix) + { + for (auto & elem : maVector) + { + elem *= rMatrix; + } + } +}; + +} + +class ImplB3DPolygon +{ + // The point vector. This vector exists always and defines the + // count of members. + CoordinateDataArray3D maPoints; + + // The BColor vector. This vectors are created on demand + // and may be zero. + std::unique_ptr<BColorArray> mpBColors; + + // The Normals vector. This vectors are created on demand + // and may be zero. + std::unique_ptr<NormalsArray3D> mpNormals; + + // The TextureCoordinates vector. This vectors are created on demand + // and may be zero. + std::unique_ptr<TextureCoordinate2D> mpTextureCoordinates; + + // The calculated plane normal. mbPlaneNormalValid says if it's valid. + ::basegfx::B3DVector maPlaneNormal; + + // flag which decides if this polygon is opened or closed + bool mbIsClosed : 1; + + // flag which says if maPlaneNormal is up-to-date + bool mbPlaneNormalValid : 1; + +protected: + void invalidatePlaneNormal() + { + if(mbPlaneNormalValid) + { + mbPlaneNormalValid = false; + } + } + +public: + // This constructor is only used from the static identity polygon, thus + // the RefCount is set to 1 to never 'delete' this static incarnation. + ImplB3DPolygon() + : maPoints(0), + maPlaneNormal(::basegfx::B3DVector::getEmptyVector()), + mbIsClosed(false), + mbPlaneNormalValid(true) + { + // complete initialization with defaults + } + + ImplB3DPolygon(const ImplB3DPolygon& rToBeCopied) + : maPoints(rToBeCopied.maPoints), + maPlaneNormal(rToBeCopied.maPlaneNormal), + mbIsClosed(rToBeCopied.mbIsClosed), + mbPlaneNormalValid(rToBeCopied.mbPlaneNormalValid) + { + // complete initialization using copy + if(rToBeCopied.mpBColors && rToBeCopied.mpBColors->isUsed()) + { + mpBColors.reset( new BColorArray(*rToBeCopied.mpBColors) ); + } + + if(rToBeCopied.mpNormals && rToBeCopied.mpNormals->isUsed()) + { + mpNormals.reset( new NormalsArray3D(*rToBeCopied.mpNormals) ); + } + + if(rToBeCopied.mpTextureCoordinates && rToBeCopied.mpTextureCoordinates->isUsed()) + { + mpTextureCoordinates.reset( new TextureCoordinate2D(*rToBeCopied.mpTextureCoordinates) ); + } + } + + ImplB3DPolygon(const ImplB3DPolygon& rToBeCopied, sal_uInt32 nIndex, sal_uInt32 nCount) + : maPoints(rToBeCopied.maPoints, nIndex, nCount), + maPlaneNormal(::basegfx::B3DVector::getEmptyVector()), + mbIsClosed(rToBeCopied.mbIsClosed), + mbPlaneNormalValid(false) + { + // complete initialization using partly copy + if(rToBeCopied.mpBColors && rToBeCopied.mpBColors->isUsed()) + { + mpBColors.reset( new BColorArray(*rToBeCopied.mpBColors, nIndex, nCount) ); + + if(!mpBColors->isUsed()) + { + mpBColors.reset(); + } + } + + if(rToBeCopied.mpNormals && rToBeCopied.mpNormals->isUsed()) + { + mpNormals.reset( new NormalsArray3D(*rToBeCopied.mpNormals, nIndex, nCount) ); + + if(!mpNormals->isUsed()) + { + mpNormals.reset(); + } + } + + if(rToBeCopied.mpTextureCoordinates && rToBeCopied.mpTextureCoordinates->isUsed()) + { + mpTextureCoordinates.reset( new TextureCoordinate2D(*rToBeCopied.mpTextureCoordinates, nIndex, nCount) ); + + if(!mpTextureCoordinates->isUsed()) + { + mpTextureCoordinates.reset(); + } + } + } + + sal_uInt32 count() const + { + return maPoints.count(); + } + + bool isClosed() const + { + return mbIsClosed; + } + + void setClosed(bool bNew) + { + if(bNew != mbIsClosed) + { + mbIsClosed = bNew; + } + } + + bool impBColorsAreEqual(const ImplB3DPolygon& rCandidate) const + { + bool bBColorsAreEqual(true); + + if(mpBColors) + { + if(rCandidate.mpBColors) + { + bBColorsAreEqual = (*mpBColors == *rCandidate.mpBColors); + } + else + { + // candidate has no BColors, so it's assumed all unused. + bBColorsAreEqual = !mpBColors->isUsed(); + } + } + else + { + if(rCandidate.mpBColors) + { + // we have no TextureCoordinates, so it's assumed all unused. + bBColorsAreEqual = !rCandidate.mpBColors->isUsed(); + } + } + + return bBColorsAreEqual; + } + + bool impNormalsAreEqual(const ImplB3DPolygon& rCandidate) const + { + bool bNormalsAreEqual(true); + + if(mpNormals) + { + if(rCandidate.mpNormals) + { + bNormalsAreEqual = (*mpNormals == *rCandidate.mpNormals); + } + else + { + // candidate has no normals, so it's assumed all unused. + bNormalsAreEqual = !mpNormals->isUsed(); + } + } + else + { + if(rCandidate.mpNormals) + { + // we have no normals, so it's assumed all unused. + bNormalsAreEqual = !rCandidate.mpNormals->isUsed(); + } + } + + return bNormalsAreEqual; + } + + bool impTextureCoordinatesAreEqual(const ImplB3DPolygon& rCandidate) const + { + bool bTextureCoordinatesAreEqual(true); + + if(mpTextureCoordinates) + { + if(rCandidate.mpTextureCoordinates) + { + bTextureCoordinatesAreEqual = (*mpTextureCoordinates == *rCandidate.mpTextureCoordinates); + } + else + { + // candidate has no TextureCoordinates, so it's assumed all unused. + bTextureCoordinatesAreEqual = !mpTextureCoordinates->isUsed(); + } + } + else + { + if(rCandidate.mpTextureCoordinates) + { + // we have no TextureCoordinates, so it's assumed all unused. + bTextureCoordinatesAreEqual = !rCandidate.mpTextureCoordinates->isUsed(); + } + } + + return bTextureCoordinatesAreEqual; + } + + bool operator==(const ImplB3DPolygon& rCandidate) const + { + if(mbIsClosed == rCandidate.mbIsClosed) + { + if(maPoints == rCandidate.maPoints) + { + if(impBColorsAreEqual(rCandidate)) + { + if(impNormalsAreEqual(rCandidate)) + { + if(impTextureCoordinatesAreEqual(rCandidate)) + { + return true; + } + } + } + } + } + + return false; + } + + const ::basegfx::B3DPoint& getPoint(sal_uInt32 nIndex) const + { + return maPoints.getCoordinate(nIndex); + } + + void setPoint(sal_uInt32 nIndex, const ::basegfx::B3DPoint& rValue) + { + maPoints.setCoordinate(nIndex, rValue); + invalidatePlaneNormal(); + } + + void insert(sal_uInt32 nIndex, const ::basegfx::B3DPoint& rPoint, sal_uInt32 nCount) + { + if(!nCount) + return; + + CoordinateData3D aCoordinate(rPoint); + maPoints.insert(nIndex, aCoordinate, nCount); + invalidatePlaneNormal(); + + if(mpBColors) + { + mpBColors->insert(nIndex, ::basegfx::BColor::getEmptyBColor(), nCount); + } + + if(mpNormals) + { + mpNormals->insert(nIndex, ::basegfx::B3DVector::getEmptyVector(), nCount); + } + + if(mpTextureCoordinates) + { + mpTextureCoordinates->insert(nIndex, ::basegfx::B2DPoint::getEmptyPoint(), nCount); + } + } + + const ::basegfx::BColor& getBColor(sal_uInt32 nIndex) const + { + if(mpBColors) + { + return mpBColors->getBColor(nIndex); + } + else + { + return ::basegfx::BColor::getEmptyBColor(); + } + } + + void setBColor(sal_uInt32 nIndex, const ::basegfx::BColor& rValue) + { + if(!mpBColors) + { + if(!rValue.equalZero()) + { + mpBColors.reset( new BColorArray(maPoints.count()) ); + mpBColors->setBColor(nIndex, rValue); + } + } + else + { + mpBColors->setBColor(nIndex, rValue); + + if(!mpBColors->isUsed()) + { + mpBColors.reset(); + } + } + } + + bool areBColorsUsed() const + { + return (mpBColors && mpBColors->isUsed()); + } + + void clearBColors() + { + mpBColors.reset(); + } + + const ::basegfx::B3DVector& getNormal() const + { + if(!mbPlaneNormalValid) + { + const_cast< ImplB3DPolygon* >(this)->maPlaneNormal = maPoints.getNormal(); + const_cast< ImplB3DPolygon* >(this)->mbPlaneNormalValid = true; + } + + return maPlaneNormal; + } + + const ::basegfx::B3DVector& getNormal(sal_uInt32 nIndex) const + { + if(mpNormals) + { + return mpNormals->getNormal(nIndex); + } + else + { + return ::basegfx::B3DVector::getEmptyVector(); + } + } + + void setNormal(sal_uInt32 nIndex, const ::basegfx::B3DVector& rValue) + { + if(!mpNormals) + { + if(!rValue.equalZero()) + { + mpNormals.reset( new NormalsArray3D(maPoints.count()) ); + mpNormals->setNormal(nIndex, rValue); + } + } + else + { + mpNormals->setNormal(nIndex, rValue); + + if(!mpNormals->isUsed()) + { + mpNormals.reset(); + } + } + } + + void transformNormals(const ::basegfx::B3DHomMatrix& rMatrix) + { + if(mpNormals) + { + mpNormals->transform(rMatrix); + } + } + + bool areNormalsUsed() const + { + return (mpNormals && mpNormals->isUsed()); + } + + void clearNormals() + { + mpNormals.reset(); + } + + const ::basegfx::B2DPoint& getTextureCoordinate(sal_uInt32 nIndex) const + { + if(mpTextureCoordinates) + { + return mpTextureCoordinates->getTextureCoordinate(nIndex); + } + else + { + return ::basegfx::B2DPoint::getEmptyPoint(); + } + } + + void setTextureCoordinate(sal_uInt32 nIndex, const ::basegfx::B2DPoint& rValue) + { + if(!mpTextureCoordinates) + { + if(!rValue.equalZero()) + { + mpTextureCoordinates.reset( new TextureCoordinate2D(maPoints.count()) ); + mpTextureCoordinates->setTextureCoordinate(nIndex, rValue); + } + } + else + { + mpTextureCoordinates->setTextureCoordinate(nIndex, rValue); + + if(!mpTextureCoordinates->isUsed()) + { + mpTextureCoordinates.reset(); + } + } + } + + bool areTextureCoordinatesUsed() const + { + return (mpTextureCoordinates && mpTextureCoordinates->isUsed()); + } + + void clearTextureCoordinates() + { + mpTextureCoordinates.reset(); + } + + void transformTextureCoordinates(const ::basegfx::B2DHomMatrix& rMatrix) + { + if(mpTextureCoordinates) + { + mpTextureCoordinates->transform(rMatrix); + } + } + + void insert(sal_uInt32 nIndex, const ImplB3DPolygon& rSource) + { + const sal_uInt32 nCount(rSource.maPoints.count()); + + if(!nCount) + return; + + maPoints.insert(nIndex, rSource.maPoints); + invalidatePlaneNormal(); + + if(rSource.mpBColors && rSource.mpBColors->isUsed()) + { + if(!mpBColors) + { + mpBColors.reset( new BColorArray(maPoints.count()) ); + } + + mpBColors->insert(nIndex, *rSource.mpBColors); + } + else + { + if(mpBColors) + { + mpBColors->insert(nIndex, ::basegfx::BColor::getEmptyBColor(), nCount); + } + } + + if(rSource.mpNormals && rSource.mpNormals->isUsed()) + { + if(!mpNormals) + { + mpNormals.reset( new NormalsArray3D(maPoints.count()) ); + } + + mpNormals->insert(nIndex, *rSource.mpNormals); + } + else + { + if(mpNormals) + { + mpNormals->insert(nIndex, ::basegfx::B3DVector::getEmptyVector(), nCount); + } + } + + if(rSource.mpTextureCoordinates && rSource.mpTextureCoordinates->isUsed()) + { + if(!mpTextureCoordinates) + { + mpTextureCoordinates.reset( new TextureCoordinate2D(maPoints.count()) ); + } + + mpTextureCoordinates->insert(nIndex, *rSource.mpTextureCoordinates); + } + else + { + if(mpTextureCoordinates) + { + mpTextureCoordinates->insert(nIndex, ::basegfx::B2DPoint::getEmptyPoint(), nCount); + } + } + } + + void remove(sal_uInt32 nIndex, sal_uInt32 nCount) + { + if(!nCount) + return; + + maPoints.remove(nIndex, nCount); + invalidatePlaneNormal(); + + if(mpBColors) + { + mpBColors->remove(nIndex, nCount); + + if(!mpBColors->isUsed()) + { + mpBColors.reset(); + } + } + + if(mpNormals) + { + mpNormals->remove(nIndex, nCount); + + if(!mpNormals->isUsed()) + { + mpNormals.reset(); + } + } + + if(mpTextureCoordinates) + { + mpTextureCoordinates->remove(nIndex, nCount); + + if(!mpTextureCoordinates->isUsed()) + { + mpTextureCoordinates.reset(); + } + } + } + + void flip() + { + if(maPoints.count() <= 1) + return; + + maPoints.flip(); + + if(mbPlaneNormalValid) + { + // mirror plane normal + maPlaneNormal = -maPlaneNormal; + } + + if(mpBColors) + { + mpBColors->flip(); + } + + if(mpNormals) + { + mpNormals->flip(); + } + + if(mpTextureCoordinates) + { + mpTextureCoordinates->flip(); + } + } + + bool hasDoublePoints() const + { + if(mbIsClosed) + { + // check for same start and end point + const sal_uInt32 nIndex(maPoints.count() - 1); + + if(maPoints.getCoordinate(0) == maPoints.getCoordinate(nIndex)) + { + const bool bBColorEqual(!mpBColors || (mpBColors->getBColor(0) == mpBColors->getBColor(nIndex))); + + if(bBColorEqual) + { + const bool bNormalsEqual(!mpNormals || (mpNormals->getNormal(0) == mpNormals->getNormal(nIndex))); + + if(bNormalsEqual) + { + const bool bTextureCoordinatesEqual(!mpTextureCoordinates || (mpTextureCoordinates->getTextureCoordinate(0) == mpTextureCoordinates->getTextureCoordinate(nIndex))); + + if(bTextureCoordinatesEqual) + { + return true; + } + } + } + } + } + + // test for range + for(sal_uInt32 a(0); a < maPoints.count() - 1; a++) + { + if(maPoints.getCoordinate(a) == maPoints.getCoordinate(a + 1)) + { + const bool bBColorEqual(!mpBColors || (mpBColors->getBColor(a) == mpBColors->getBColor(a + 1))); + + if(bBColorEqual) + { + const bool bNormalsEqual(!mpNormals || (mpNormals->getNormal(a) == mpNormals->getNormal(a + 1))); + + if(bNormalsEqual) + { + const bool bTextureCoordinatesEqual(!mpTextureCoordinates || (mpTextureCoordinates->getTextureCoordinate(a) == mpTextureCoordinates->getTextureCoordinate(a + 1))); + + if(bTextureCoordinatesEqual) + { + return true; + } + } + } + } + } + + return false; + } + + void removeDoublePointsAtBeginEnd() + { + // Only remove DoublePoints at Begin and End when poly is closed + if(!mbIsClosed) + return; + + bool bRemove; + + do + { + bRemove = false; + + if(maPoints.count() > 1) + { + const sal_uInt32 nIndex(maPoints.count() - 1); + bRemove = (maPoints.getCoordinate(0) == maPoints.getCoordinate(nIndex)); + + if(bRemove && mpBColors && mpBColors->getBColor(0) != mpBColors->getBColor(nIndex)) + { + bRemove = false; + } + + if(bRemove && mpNormals && mpNormals->getNormal(0) != mpNormals->getNormal(nIndex)) + { + bRemove = false; + } + + if(bRemove && mpTextureCoordinates && mpTextureCoordinates->getTextureCoordinate(0) != mpTextureCoordinates->getTextureCoordinate(nIndex)) + { + bRemove = false; + } + } + + if(bRemove) + { + const sal_uInt32 nIndex(maPoints.count() - 1); + remove(nIndex, 1); + } + } while(bRemove); + } + + void removeDoublePointsWholeTrack() + { + sal_uInt32 nIndex(0); + + // test as long as there are at least two points and as long as the index + // is smaller or equal second last point + while((maPoints.count() > 1) && (nIndex <= maPoints.count() - 2)) + { + const sal_uInt32 nNextIndex(nIndex + 1); + bool bRemove(maPoints.getCoordinate(nIndex) == maPoints.getCoordinate(nNextIndex)); + + if(bRemove && mpBColors && mpBColors->getBColor(nIndex) != mpBColors->getBColor(nNextIndex)) + { + bRemove = false; + } + + if(bRemove && mpNormals && mpNormals->getNormal(nIndex) != mpNormals->getNormal(nNextIndex)) + { + bRemove = false; + } + + if(bRemove && mpTextureCoordinates && mpTextureCoordinates->getTextureCoordinate(nIndex) != mpTextureCoordinates->getTextureCoordinate(nNextIndex)) + { + bRemove = false; + } + + if(bRemove) + { + // if next is same as index and the control vectors are unused, delete index + remove(nIndex, 1); + } + else + { + // if different, step forward + nIndex++; + } + } + } + + void transform(const ::basegfx::B3DHomMatrix& rMatrix) + { + maPoints.transform(rMatrix); + + // Here, it seems to be possible to transform a valid plane normal and to avoid + // invalidation, but it's not true. If the transformation contains shears or e.g. + // perspective projection, the orthogonality to the transformed plane will not + // be preserved. It may be possible to test that at the matrix to not invalidate in + // all cases or to extract a matrix which does not 'shear' the vector which is + // a normal in this case. As long as this is not sure, i will just invalidate. + invalidatePlaneNormal(); + } +}; + +namespace basegfx +{ + namespace { + + B3DPolygon::ImplType const & getDefaultPolygon() { + static B3DPolygon::ImplType const singleton; + return singleton; + } + + } + + B3DPolygon::B3DPolygon() : + mpPolygon(getDefaultPolygon()) + { + } + + B3DPolygon::B3DPolygon(const B3DPolygon&) = default; + + B3DPolygon::B3DPolygon(B3DPolygon&&) = default; + + B3DPolygon::~B3DPolygon() = default; + + B3DPolygon& B3DPolygon::operator=(const B3DPolygon&) = default; + + B3DPolygon& B3DPolygon::operator=(B3DPolygon&&) = default; + + bool B3DPolygon::operator==(const B3DPolygon& rPolygon) const + { + if(mpPolygon.same_object(rPolygon.mpPolygon)) + return true; + + return (*mpPolygon == *rPolygon.mpPolygon); + } + + sal_uInt32 B3DPolygon::count() const + { + return mpPolygon->count(); + } + + basegfx::B3DPoint const & B3DPolygon::getB3DPoint(sal_uInt32 nIndex) const + { + OSL_ENSURE(nIndex < mpPolygon->count(), "B3DPolygon access outside range (!)"); + + return mpPolygon->getPoint(nIndex); + } + + void B3DPolygon::setB3DPoint(sal_uInt32 nIndex, const basegfx::B3DPoint& rValue) + { + OSL_ENSURE(nIndex < std::as_const(mpPolygon)->count(), "B3DPolygon access outside range (!)"); + + if(getB3DPoint(nIndex) != rValue) + mpPolygon->setPoint(nIndex, rValue); + } + + BColor const & B3DPolygon::getBColor(sal_uInt32 nIndex) const + { + OSL_ENSURE(nIndex < mpPolygon->count(), "B3DPolygon access outside range (!)"); + + return mpPolygon->getBColor(nIndex); + } + + void B3DPolygon::setBColor(sal_uInt32 nIndex, const BColor& rValue) + { + OSL_ENSURE(nIndex < std::as_const(mpPolygon)->count(), "B3DPolygon access outside range (!)"); + + if(std::as_const(mpPolygon)->getBColor(nIndex) != rValue) + mpPolygon->setBColor(nIndex, rValue); + } + + bool B3DPolygon::areBColorsUsed() const + { + return mpPolygon->areBColorsUsed(); + } + + void B3DPolygon::clearBColors() + { + if(std::as_const(mpPolygon)->areBColorsUsed()) + mpPolygon->clearBColors(); + } + + B3DVector const & B3DPolygon::getNormal() const + { + return mpPolygon->getNormal(); + } + + B3DVector const & B3DPolygon::getNormal(sal_uInt32 nIndex) const + { + OSL_ENSURE(nIndex < mpPolygon->count(), "B3DPolygon access outside range (!)"); + + return mpPolygon->getNormal(nIndex); + } + + void B3DPolygon::setNormal(sal_uInt32 nIndex, const B3DVector& rValue) + { + OSL_ENSURE(nIndex < std::as_const(mpPolygon)->count(), "B3DPolygon access outside range (!)"); + + if(std::as_const(mpPolygon)->getNormal(nIndex) != rValue) + mpPolygon->setNormal(nIndex, rValue); + } + + void B3DPolygon::transformNormals(const B3DHomMatrix& rMatrix) + { + if(std::as_const(mpPolygon)->areNormalsUsed() && !rMatrix.isIdentity()) + mpPolygon->transformNormals(rMatrix); + } + + bool B3DPolygon::areNormalsUsed() const + { + return mpPolygon->areNormalsUsed(); + } + + void B3DPolygon::clearNormals() + { + if(std::as_const(mpPolygon)->areNormalsUsed()) + mpPolygon->clearNormals(); + } + + B2DPoint const & B3DPolygon::getTextureCoordinate(sal_uInt32 nIndex) const + { + OSL_ENSURE(nIndex < mpPolygon->count(), "B3DPolygon access outside range (!)"); + + return mpPolygon->getTextureCoordinate(nIndex); + } + + void B3DPolygon::setTextureCoordinate(sal_uInt32 nIndex, const B2DPoint& rValue) + { + OSL_ENSURE(nIndex < std::as_const(mpPolygon)->count(), "B3DPolygon access outside range (!)"); + + if(std::as_const(mpPolygon)->getTextureCoordinate(nIndex) != rValue) + mpPolygon->setTextureCoordinate(nIndex, rValue); + } + + void B3DPolygon::transformTextureCoordinates(const B2DHomMatrix& rMatrix) + { + if(std::as_const(mpPolygon)->areTextureCoordinatesUsed() && !rMatrix.isIdentity()) + mpPolygon->transformTextureCoordinates(rMatrix); + } + + bool B3DPolygon::areTextureCoordinatesUsed() const + { + return mpPolygon->areTextureCoordinatesUsed(); + } + + void B3DPolygon::clearTextureCoordinates() + { + if(std::as_const(mpPolygon)->areTextureCoordinatesUsed()) + mpPolygon->clearTextureCoordinates(); + } + + void B3DPolygon::append(const basegfx::B3DPoint& rPoint, sal_uInt32 nCount) + { + if(nCount) + mpPolygon->insert(std::as_const(mpPolygon)->count(), rPoint, nCount); + } + + void B3DPolygon::append(const B3DPolygon& rPoly, sal_uInt32 nIndex, sal_uInt32 nCount) + { + if(!rPoly.count()) + return; + + if(!nCount) + { + nCount = rPoly.count(); + } + + if(nIndex == 0 && nCount == rPoly.count()) + { + mpPolygon->insert(std::as_const(mpPolygon)->count(), *rPoly.mpPolygon); + } + else + { + OSL_ENSURE(nIndex + nCount <= rPoly.mpPolygon->count(), "B3DPolygon Append outside range (!)"); + ImplB3DPolygon aTempPoly(*rPoly.mpPolygon, nIndex, nCount); + mpPolygon->insert(std::as_const(mpPolygon)->count(), aTempPoly); + } + } + + void B3DPolygon::remove(sal_uInt32 nIndex, sal_uInt32 nCount) + { + OSL_ENSURE(nIndex + nCount <= std::as_const(mpPolygon)->count(), "B3DPolygon Remove outside range (!)"); + + if(nCount) + mpPolygon->remove(nIndex, nCount); + } + + void B3DPolygon::clear() + { + mpPolygon = getDefaultPolygon(); + } + + bool B3DPolygon::isClosed() const + { + return mpPolygon->isClosed(); + } + + void B3DPolygon::setClosed(bool bNew) + { + if(isClosed() != bNew) + mpPolygon->setClosed(bNew); + } + + void B3DPolygon::flip() + { + if(count() > 1) + mpPolygon->flip(); + } + + bool B3DPolygon::hasDoublePoints() const + { + return (mpPolygon->count() > 1 && mpPolygon->hasDoublePoints()); + } + + void B3DPolygon::removeDoublePoints() + { + if(hasDoublePoints()) + { + mpPolygon->removeDoublePointsAtBeginEnd(); + mpPolygon->removeDoublePointsWholeTrack(); + } + } + + void B3DPolygon::transform(const basegfx::B3DHomMatrix& rMatrix) + { + if(std::as_const(mpPolygon)->count() && !rMatrix.isIdentity()) + { + mpPolygon->transform(rMatrix); + } + } +} // end of namespace basegfx + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/basegfx/source/polygon/b3dpolygontools.cxx b/basegfx/source/polygon/b3dpolygontools.cxx new file mode 100644 index 0000000000..7c92f5ddce --- /dev/null +++ b/basegfx/source/polygon/b3dpolygontools.cxx @@ -0,0 +1,826 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * 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 . + */ + +#include <osl/diagnose.h> +#include <basegfx/polygon/b3dpolygontools.hxx> +#include <basegfx/polygon/b3dpolygon.hxx> +#include <basegfx/polygon/b3dpolypolygon.hxx> +#include <basegfx/numeric/ftools.hxx> +#include <basegfx/range/b3drange.hxx> +#include <basegfx/point/b2dpoint.hxx> +#include <basegfx/tuple/b3ituple.hxx> +#include <cassert> +#include <numeric> + +namespace basegfx::utils +{ + // B3DPolygon tools + void checkClosed(B3DPolygon& rCandidate) + { + while(rCandidate.count() > 1 + && rCandidate.getB3DPoint(0).equal(rCandidate.getB3DPoint(rCandidate.count() - 1))) + { + rCandidate.setClosed(true); + rCandidate.remove(rCandidate.count() - 1); + } + } + + sal_uInt32 getIndexOfSuccessor(sal_uInt32 nIndex, const B3DPolygon& rCandidate) + { + OSL_ENSURE(nIndex < rCandidate.count(), "getIndexOfPredecessor: Access to polygon out of range (!)"); + + if(nIndex + 1 < rCandidate.count()) + { + return nIndex + 1; + } + else + { + return 0; + } + } + + B3DRange getRange(const B3DPolygon& rCandidate) + { + B3DRange aRetval; + const sal_uInt32 nPointCount(rCandidate.count()); + + for(sal_uInt32 a(0); a < nPointCount; a++) + { + const B3DPoint aTestPoint(rCandidate.getB3DPoint(a)); + aRetval.expand(aTestPoint); + } + + return aRetval; + } + + double getLength(const B3DPolygon& rCandidate) + { + double fRetval(0.0); + const sal_uInt32 nPointCount(rCandidate.count()); + + if(nPointCount > 1) + { + const sal_uInt32 nLoopCount(rCandidate.isClosed() ? nPointCount : nPointCount - 1); + + for(sal_uInt32 a(0); a < nLoopCount; a++) + { + const sal_uInt32 nNextIndex(getIndexOfSuccessor(a, rCandidate)); + const B3DPoint aCurrentPoint(rCandidate.getB3DPoint(a)); + const B3DPoint aNextPoint(rCandidate.getB3DPoint(nNextIndex)); + const B3DVector aVector(aNextPoint - aCurrentPoint); + fRetval += aVector.getLength(); + } + } + + return fRetval; + } + + void applyLineDashing( + const B3DPolygon& rCandidate, + const std::vector<double>& rDotDashArray, + B3DPolyPolygon* pLineTarget, + double fDotDashLength) + { + // clear targets in any case + if(pLineTarget) + { + pLineTarget->clear(); + } + + // provide callback as lambda + auto aLineCallback( + nullptr == pLineTarget + ? std::function<void(const basegfx::B3DPolygon&)>() + : [&pLineTarget](const basegfx::B3DPolygon& rSnippet){ pLineTarget->append(rSnippet); }); + + // call version that uses callbacks + applyLineDashing( + rCandidate, + rDotDashArray, + aLineCallback, + fDotDashLength); + } + + static void implHandleSnippet( + const B3DPolygon& rSnippet, + const std::function<void(const basegfx::B3DPolygon& rSnippet)>& rTargetCallback, + B3DPolygon& rFirst, + B3DPolygon& rLast) + { + if(rSnippet.isClosed()) + { + if(!rFirst.count()) + { + rFirst = rSnippet; + } + else + { + if(rLast.count()) + { + rTargetCallback(rLast); + } + + rLast = rSnippet; + } + } + else + { + rTargetCallback(rSnippet); + } + } + + static void implHandleFirstLast( + const std::function<void(const basegfx::B3DPolygon& rSnippet)>& rTargetCallback, + B3DPolygon& rFirst, + B3DPolygon& rLast) + { + if(rFirst.count() && rLast.count() + && rFirst.getB3DPoint(0).equal(rLast.getB3DPoint(rLast.count() - 1))) + { + // start of first and end of last are the same -> merge them + rLast.append(rFirst); + rLast.removeDoublePoints(); + rFirst.clear(); + } + + if(rLast.count()) + { + rTargetCallback(rLast); + } + + if(rFirst.count()) + { + rTargetCallback(rFirst); + } + } + + void applyLineDashing( + const B3DPolygon& rCandidate, + const std::vector<double>& rDotDashArray, + const std::function<void(const basegfx::B3DPolygon& rSnippet)>& rLineTargetCallback, + double fDotDashLength) + { + const sal_uInt32 nPointCount(rCandidate.count()); + const sal_uInt32 nDotDashCount(rDotDashArray.size()); + + if(fTools::lessOrEqual(fDotDashLength, 0.0)) + { + fDotDashLength = std::accumulate(rDotDashArray.begin(), rDotDashArray.end(), 0.0); + } + + if(fTools::lessOrEqual(fDotDashLength, 0.0) || !rLineTargetCallback || !nPointCount) + { + // parameters make no sense, just add source to targets + if (rLineTargetCallback) + rLineTargetCallback(rCandidate); + return; + } + + // precalculate maximal acceptable length of candidate polygon assuming + // we want to create a maximum of fNumberOfAllowedSnippets. In 3D + // use less for fNumberOfAllowedSnippets, ca. 6553.6, double due to line & gap. + // Less in 3D due to potentially blowing up to rounded line segments. + static const double fNumberOfAllowedSnippets(6553.5 * 2.0); + const double fAllowedLength((fNumberOfAllowedSnippets * fDotDashLength) / double(rDotDashArray.size())); + const double fCandidateLength(basegfx::utils::getLength(rCandidate)); + std::vector<double> aDotDashArray(rDotDashArray); + + if(fCandidateLength > fAllowedLength) + { + // we would produce more than fNumberOfAllowedSnippets, so + // adapt aDotDashArray to exactly produce assumed number. Also + // assert this to let the caller know about it. + // If this asserts: Please think about checking your DotDashArray + // before calling this function or evtl. use the callback version + // to *not* produce that much of data. Even then, you may still + // think about producing too much runtime (!) + assert(true && "applyLineDashing: potentially too expensive to do the requested dismantle - please consider stretched LineDash pattern (!)"); + + // calculate correcting factor, apply to aDotDashArray and fDotDashLength + // to enlarge these as needed + const double fFactor(fCandidateLength / fAllowedLength); + std::for_each(aDotDashArray.begin(), aDotDashArray.end(), [&fFactor](double &f){ f *= fFactor; }); + } + + // prepare current edge's start + B3DPoint aCurrentPoint(rCandidate.getB3DPoint(0)); + const bool bIsClosed(rCandidate.isClosed()); + const sal_uInt32 nEdgeCount(bIsClosed ? nPointCount : nPointCount - 1); + + // prepare DotDashArray iteration and the line/gap switching bool + sal_uInt32 nDotDashIndex(0); + bool bIsLine(true); + double fDotDashMovingLength(aDotDashArray[0]); + B3DPolygon aSnippet; + + // remember 1st and last snippets to try to merge after execution + // is complete and hand to callback + B3DPolygon aFirstLine, aLastLine; + + // iterate over all edges + for(sal_uInt32 a(0); a < nEdgeCount; a++) + { + // update current edge + const sal_uInt32 nNextIndex((a + 1) % nPointCount); + const B3DPoint aNextPoint(rCandidate.getB3DPoint(nNextIndex)); + const double fEdgeLength(B3DVector(aNextPoint - aCurrentPoint).getLength()); + + if(!fTools::equalZero(fEdgeLength)) + { + double fLastDotDashMovingLength(0.0); + while(fTools::less(fDotDashMovingLength, fEdgeLength)) + { + // new split is inside edge, create and append snippet [fLastDotDashMovingLength, fDotDashMovingLength] + if(bIsLine) + { + if(!aSnippet.count()) + { + aSnippet.append(interpolate(aCurrentPoint, aNextPoint, fLastDotDashMovingLength / fEdgeLength)); + } + + aSnippet.append(interpolate(aCurrentPoint, aNextPoint, fDotDashMovingLength / fEdgeLength)); + + implHandleSnippet(aSnippet, rLineTargetCallback, aFirstLine, aLastLine); + + aSnippet.clear(); + } + + // prepare next DotDashArray step and flip line/gap flag + fLastDotDashMovingLength = fDotDashMovingLength; + fDotDashMovingLength += aDotDashArray[(++nDotDashIndex) % nDotDashCount]; + bIsLine = !bIsLine; + } + + // append snippet [fLastDotDashMovingLength, fEdgeLength] + if(bIsLine) + { + if(!aSnippet.count()) + { + aSnippet.append(interpolate(aCurrentPoint, aNextPoint, fLastDotDashMovingLength / fEdgeLength)); + } + + aSnippet.append(aNextPoint); + } + + // prepare move to next edge + fDotDashMovingLength -= fEdgeLength; + } + + // prepare next edge step (end point gets new start point) + aCurrentPoint = aNextPoint; + } + + // append last intermediate results (if exists) + if(aSnippet.count()) + { + if(bIsLine) + { + implHandleSnippet(aSnippet, rLineTargetCallback, aFirstLine, aLastLine); + } + } + + if(bIsClosed) + { + implHandleFirstLast(rLineTargetCallback, aFirstLine, aLastLine); + } + } + + B3DPolygon applyDefaultNormalsSphere( const B3DPolygon& rCandidate, const B3DPoint& rCenter) + { + B3DPolygon aRetval(rCandidate); + + for(sal_uInt32 a(0); a < aRetval.count(); a++) + { + B3DVector aVector(aRetval.getB3DPoint(a) - rCenter); + aVector.normalize(); + aRetval.setNormal(a, aVector); + } + + return aRetval; + } + + B3DPolygon invertNormals( const B3DPolygon& rCandidate) + { + B3DPolygon aRetval(rCandidate); + + if(aRetval.areNormalsUsed()) + { + for(sal_uInt32 a(0); a < aRetval.count(); a++) + { + aRetval.setNormal(a, -aRetval.getNormal(a)); + } + } + + return aRetval; + } + + B3DPolygon applyDefaultTextureCoordinatesParallel( const B3DPolygon& rCandidate, const B3DRange& rRange, bool bChangeX, bool bChangeY) + { + B3DPolygon aRetval(rCandidate); + + if(bChangeX || bChangeY) + { + // create projection of standard texture coordinates in (X, Y) onto + // the 3d coordinates straight + const double fWidth(rRange.getWidth()); + const double fHeight(rRange.getHeight()); + const bool bWidthSet(!fTools::equalZero(fWidth)); + const bool bHeightSet(!fTools::equalZero(fHeight)); + const double fOne(1.0); + + for(sal_uInt32 a(0); a < aRetval.count(); a++) + { + const B3DPoint aPoint(aRetval.getB3DPoint(a)); + B2DPoint aTextureCoordinate(aRetval.getTextureCoordinate(a)); + + if(bChangeX) + { + if(bWidthSet) + { + aTextureCoordinate.setX((aPoint.getX() - rRange.getMinX()) / fWidth); + } + else + { + aTextureCoordinate.setX(0.0); + } + } + + if(bChangeY) + { + if(bHeightSet) + { + aTextureCoordinate.setY(fOne - ((aPoint.getY() - rRange.getMinY()) / fHeight)); + } + else + { + aTextureCoordinate.setY(fOne); + } + } + + aRetval.setTextureCoordinate(a, aTextureCoordinate); + } + } + + return aRetval; + } + + B3DPolygon applyDefaultTextureCoordinatesSphere( const B3DPolygon& rCandidate, const B3DPoint& rCenter, bool bChangeX, bool bChangeY) + { + B3DPolygon aRetval(rCandidate); + + if(bChangeX || bChangeY) + { + // create texture coordinates using sphere projection to cartesian coordinates, + // use object's center as base + const double fOne(1.0); + const sal_uInt32 nPointCount(aRetval.count()); + bool bPolarPoints(false); + sal_uInt32 a; + + // create center cartesian coordinates to have a possibility to decide if on boundary + // transitions which value to choose + const B3DRange aPlaneRange(getRange(rCandidate)); + const B3DPoint aPlaneCenter(aPlaneRange.getCenter() - rCenter); + const double fXCenter(fOne - ((atan2(aPlaneCenter.getZ(), aPlaneCenter.getX()) + M_PI) / (2 * M_PI))); + + for(a = 0; a < nPointCount; a++) + { + const B3DVector aVector(aRetval.getB3DPoint(a) - rCenter); + const double fY(fOne - ((atan2(aVector.getY(), aVector.getXZLength()) + M_PI_2) / M_PI)); + B2DPoint aTexCoor(aRetval.getTextureCoordinate(a)); + + if(fTools::equalZero(fY)) + { + // point is a north polar point, no useful X-coordinate can be created. + if(bChangeY) + { + aTexCoor.setY(0.0); + + if(bChangeX) + { + bPolarPoints = true; + } + } + } + else if(fTools::equal(fY, fOne)) + { + // point is a south polar point, no useful X-coordinate can be created. Set + // Y-coordinate, though + if(bChangeY) + { + aTexCoor.setY(fOne); + + if(bChangeX) + { + bPolarPoints = true; + } + } + } + else + { + double fX(fOne - ((atan2(aVector.getZ(), aVector.getX()) + M_PI) / (2 * M_PI))); + + // correct cartesian point coordinate dependent from center value + if(fX > fXCenter + 0.5) + { + fX -= fOne; + } + else if(fX < fXCenter - 0.5) + { + fX += fOne; + } + + if(bChangeX) + { + aTexCoor.setX(fX); + } + + if(bChangeY) + { + aTexCoor.setY(fY); + } + } + + aRetval.setTextureCoordinate(a, aTexCoor); + } + + if(bPolarPoints) + { + // correct X-texture coordinates if polar points are contained. Those + // coordinates cannot be correct, so use prev or next X-coordinate + for(a = 0; a < nPointCount; a++) + { + B2DPoint aTexCoor(aRetval.getTextureCoordinate(a)); + + if(fTools::equalZero(aTexCoor.getY()) || fTools::equal(aTexCoor.getY(), fOne)) + { + // get prev, next TexCoor and test for pole + const B2DPoint aPrevTexCoor(aRetval.getTextureCoordinate(a ? a - 1 : nPointCount - 1)); + const B2DPoint aNextTexCoor(aRetval.getTextureCoordinate((a + 1) % nPointCount)); + const bool bPrevPole(fTools::equalZero(aPrevTexCoor.getY()) || fTools::equal(aPrevTexCoor.getY(), fOne)); + const bool bNextPole(fTools::equalZero(aNextTexCoor.getY()) || fTools::equal(aNextTexCoor.getY(), fOne)); + + if(!bPrevPole && !bNextPole) + { + // both no poles, mix them + aTexCoor.setX((aPrevTexCoor.getX() + aNextTexCoor.getX()) / 2.0); + } + else if(!bNextPole) + { + // copy next + aTexCoor.setX(aNextTexCoor.getX()); + } + else + { + // copy prev, even if it's a pole, hopefully it is already corrected + aTexCoor.setX(aPrevTexCoor.getX()); + } + + aRetval.setTextureCoordinate(a, aTexCoor); + } + } + } + } + + return aRetval; + } + + bool isInside(const B3DPolygon& rCandidate, const B3DPoint& rPoint, bool bWithBorder) + { + if(bWithBorder && isPointOnPolygon(rCandidate, rPoint)) + { + return true; + } + else + { + bool bRetval(false); + const B3DVector aPlaneNormal(rCandidate.getNormal()); + + if(!aPlaneNormal.equalZero()) + { + const sal_uInt32 nPointCount(rCandidate.count()); + + if(nPointCount) + { + B3DPoint aCurrentPoint(rCandidate.getB3DPoint(nPointCount - 1)); + const double fAbsX(fabs(aPlaneNormal.getX())); + const double fAbsY(fabs(aPlaneNormal.getY())); + const double fAbsZ(fabs(aPlaneNormal.getZ())); + + if(fAbsX > fAbsY && fAbsX > fAbsZ) + { + // normal points mostly in X-Direction, use YZ-Polygon projection for check + // x -> y, y -> z + for(sal_uInt32 a(0); a < nPointCount; a++) + { + const B3DPoint aPreviousPoint(aCurrentPoint); + aCurrentPoint = rCandidate.getB3DPoint(a); + + // cross-over in Z? + const bool bCompZA(fTools::more(aPreviousPoint.getZ(), rPoint.getZ())); + const bool bCompZB(fTools::more(aCurrentPoint.getZ(), rPoint.getZ())); + + if(bCompZA != bCompZB) + { + // cross-over in Y? + const bool bCompYA(fTools::more(aPreviousPoint.getY(), rPoint.getY())); + const bool bCompYB(fTools::more(aCurrentPoint.getY(), rPoint.getY())); + + if(bCompYA == bCompYB) + { + if(bCompYA) + { + bRetval = !bRetval; + } + } + else + { + const double fCompare( + aCurrentPoint.getY() - (aCurrentPoint.getZ() - rPoint.getZ()) * + (aPreviousPoint.getY() - aCurrentPoint.getY()) / + (aPreviousPoint.getZ() - aCurrentPoint.getZ())); + + if(fTools::more(fCompare, rPoint.getY())) + { + bRetval = !bRetval; + } + } + } + } + } + else if(fAbsY > fAbsX && fAbsY > fAbsZ) + { + // normal points mostly in Y-Direction, use XZ-Polygon projection for check + // x -> x, y -> z + for(sal_uInt32 a(0); a < nPointCount; a++) + { + const B3DPoint aPreviousPoint(aCurrentPoint); + aCurrentPoint = rCandidate.getB3DPoint(a); + + // cross-over in Z? + const bool bCompZA(fTools::more(aPreviousPoint.getZ(), rPoint.getZ())); + const bool bCompZB(fTools::more(aCurrentPoint.getZ(), rPoint.getZ())); + + if(bCompZA != bCompZB) + { + // cross-over in X? + const bool bCompXA(fTools::more(aPreviousPoint.getX(), rPoint.getX())); + const bool bCompXB(fTools::more(aCurrentPoint.getX(), rPoint.getX())); + + if(bCompXA == bCompXB) + { + if(bCompXA) + { + bRetval = !bRetval; + } + } + else + { + const double fCompare( + aCurrentPoint.getX() - (aCurrentPoint.getZ() - rPoint.getZ()) * + (aPreviousPoint.getX() - aCurrentPoint.getX()) / + (aPreviousPoint.getZ() - aCurrentPoint.getZ())); + + if(fTools::more(fCompare, rPoint.getX())) + { + bRetval = !bRetval; + } + } + } + } + } + else + { + // normal points mostly in Z-Direction, use XY-Polygon projection for check + // x -> x, y -> y + for(sal_uInt32 a(0); a < nPointCount; a++) + { + const B3DPoint aPreviousPoint(aCurrentPoint); + aCurrentPoint = rCandidate.getB3DPoint(a); + + // cross-over in Y? + const bool bCompYA(fTools::more(aPreviousPoint.getY(), rPoint.getY())); + const bool bCompYB(fTools::more(aCurrentPoint.getY(), rPoint.getY())); + + if(bCompYA != bCompYB) + { + // cross-over in X? + const bool bCompXA(fTools::more(aPreviousPoint.getX(), rPoint.getX())); + const bool bCompXB(fTools::more(aCurrentPoint.getX(), rPoint.getX())); + + if(bCompXA == bCompXB) + { + if(bCompXA) + { + bRetval = !bRetval; + } + } + else + { + const double fCompare( + aCurrentPoint.getX() - (aCurrentPoint.getY() - rPoint.getY()) * + (aPreviousPoint.getX() - aCurrentPoint.getX()) / + (aPreviousPoint.getY() - aCurrentPoint.getY())); + + if(fTools::more(fCompare, rPoint.getX())) + { + bRetval = !bRetval; + } + } + } + } + } + } + } + + return bRetval; + } + } + + bool isPointOnLine(const B3DPoint& rStart, const B3DPoint& rEnd, const B3DPoint& rCandidate, bool bWithPoints) + { + if(rCandidate.equal(rStart) || rCandidate.equal(rEnd)) + { + // candidate is in epsilon around start or end -> inside + return bWithPoints; + } + else if(rStart.equal(rEnd)) + { + // start and end are equal, but candidate is outside their epsilon -> outside + return false; + } + else + { + const B3DVector aEdgeVector(rEnd - rStart); + const B3DVector aTestVector(rCandidate - rStart); + + if(areParallel(aEdgeVector, aTestVector)) + { + double fParamTestOnCurr(0.0); + + if(aEdgeVector.getX() > aEdgeVector.getY()) + { + if(aEdgeVector.getX() > aEdgeVector.getZ()) + { + // X is biggest + fParamTestOnCurr = aTestVector.getX() / aEdgeVector.getX(); + } + else + { + // Z is biggest + fParamTestOnCurr = aTestVector.getZ() / aEdgeVector.getZ(); + } + } + else + { + if(aEdgeVector.getY() > aEdgeVector.getZ()) + { + // Y is biggest + fParamTestOnCurr = aTestVector.getY() / aEdgeVector.getY(); + } + else + { + // Z is biggest + fParamTestOnCurr = aTestVector.getZ() / aEdgeVector.getZ(); + } + } + + if(fTools::more(fParamTestOnCurr, 0.0) && fTools::less(fParamTestOnCurr, 1.0)) + { + return true; + } + } + + return false; + } + } + + bool isPointOnPolygon(const B3DPolygon& rCandidate, const B3DPoint& rPoint) + { + const sal_uInt32 nPointCount(rCandidate.count()); + + if(nPointCount > 1) + { + const sal_uInt32 nLoopCount(rCandidate.isClosed() ? nPointCount : nPointCount - 1); + B3DPoint aCurrentPoint(rCandidate.getB3DPoint(0)); + + for(sal_uInt32 a(0); a < nLoopCount; a++) + { + const B3DPoint aNextPoint(rCandidate.getB3DPoint((a + 1) % nPointCount)); + + if(isPointOnLine(aCurrentPoint, aNextPoint, rPoint, true/*bWithPoints*/)) + { + return true; + } + + aCurrentPoint = aNextPoint; + } + } + else if(nPointCount) + { + return rPoint.equal(rCandidate.getB3DPoint(0)); + } + + return false; + } + + bool getCutBetweenLineAndPlane(const B3DVector& rPlaneNormal, const B3DPoint& rPlanePoint, const B3DPoint& rEdgeStart, const B3DPoint& rEdgeEnd, double& fCut) + { + if(!rPlaneNormal.equalZero() && !rEdgeStart.equal(rEdgeEnd)) + { + const B3DVector aTestEdge(rEdgeEnd - rEdgeStart); + const double fScalarEdge(rPlaneNormal.scalar(aTestEdge)); + + if(!fTools::equalZero(fScalarEdge)) + { + const B3DVector aCompareEdge(rPlanePoint - rEdgeStart); + const double fScalarCompare(rPlaneNormal.scalar(aCompareEdge)); + + fCut = fScalarCompare / fScalarEdge; + return true; + } + } + + return false; + } + + // snap points of horizontal or vertical edges to discrete values + B3DPolygon snapPointsOfHorizontalOrVerticalEdges(const B3DPolygon& rCandidate) + { + const sal_uInt32 nPointCount(rCandidate.count()); + + if(nPointCount > 1) + { + // Start by copying the source polygon to get a writeable copy. The closed state is + // copied by aRetval's initialisation, too, so no need to copy it in this method + B3DPolygon aRetval(rCandidate); + + // prepare geometry data. Get rounded from original + B3ITuple aPrevTuple(basegfx::fround(rCandidate.getB3DPoint(nPointCount - 1))); + B3DPoint aCurrPoint(rCandidate.getB3DPoint(0)); + B3ITuple aCurrTuple(basegfx::fround(aCurrPoint)); + + // loop over all points. This will also snap the implicit closing edge + // even when not closed, but that's no problem here + for(sal_uInt32 a(0); a < nPointCount; a++) + { + // get next point. Get rounded from original + const bool bLastRun(a + 1 == nPointCount); + const sal_uInt32 nNextIndex(bLastRun ? 0 : a + 1); + const B3DPoint aNextPoint(rCandidate.getB3DPoint(nNextIndex)); + const B3ITuple aNextTuple(basegfx::fround(aNextPoint)); + + // get the states + const bool bPrevVertical(aPrevTuple.getX() == aCurrTuple.getX()); + const bool bNextVertical(aNextTuple.getX() == aCurrTuple.getX()); + const bool bPrevHorizontal(aPrevTuple.getY() == aCurrTuple.getY()); + const bool bNextHorizontal(aNextTuple.getY() == aCurrTuple.getY()); + const bool bSnapX(bPrevVertical || bNextVertical); + const bool bSnapY(bPrevHorizontal || bNextHorizontal); + + if(bSnapX || bSnapY) + { + const B3DPoint aSnappedPoint( + bSnapX ? aCurrTuple.getX() : aCurrPoint.getX(), + bSnapY ? aCurrTuple.getY() : aCurrPoint.getY(), + aCurrPoint.getZ()); + + aRetval.setB3DPoint(a, aSnappedPoint); + } + + // prepare next point + if(!bLastRun) + { + aPrevTuple = aCurrTuple; + aCurrPoint = aNextPoint; + aCurrTuple = aNextTuple; + } + } + + return aRetval; + } + else + { + return rCandidate; + } + } + +} // end of namespace + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/basegfx/source/polygon/b3dpolypolygon.cxx b/basegfx/source/polygon/b3dpolypolygon.cxx new file mode 100644 index 0000000000..017906eef5 --- /dev/null +++ b/basegfx/source/polygon/b3dpolypolygon.cxx @@ -0,0 +1,400 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * 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 . + */ + +#include <osl/diagnose.h> +#include <basegfx/polygon/b3dpolypolygon.hxx> +#include <basegfx/polygon/b3dpolygon.hxx> +#include <basegfx/matrix/b2dhommatrix.hxx> +#include <basegfx/matrix/b3dhommatrix.hxx> +#include <utility> +#include <vector> + +class ImplB3DPolyPolygon +{ + typedef std::vector< ::basegfx::B3DPolygon > PolygonVector; + + PolygonVector maPolygons; + +public: + ImplB3DPolyPolygon() + { + } + + explicit ImplB3DPolyPolygon(const ::basegfx::B3DPolygon& rToBeCopied) : + maPolygons(1,rToBeCopied) + { + } + + bool operator==(const ImplB3DPolyPolygon& rPolygonList) const + { + // same polygon count? + if(maPolygons.size() != rPolygonList.maPolygons.size()) + return false; + + // compare polygon content + if(maPolygons != rPolygonList.maPolygons) + return false; + + return true; + } + + const ::basegfx::B3DPolygon& getB3DPolygon(sal_uInt32 nIndex) const + { + return maPolygons[nIndex]; + } + + void setB3DPolygon(sal_uInt32 nIndex, const ::basegfx::B3DPolygon& rPolygon) + { + maPolygons[nIndex] = rPolygon; + } + + void insert(sal_uInt32 nIndex, const ::basegfx::B3DPolygon& rPolygon, sal_uInt32 nCount) + { + if(nCount) + { + // add nCount copies of rPolygon + PolygonVector::iterator aIndex(maPolygons.begin()); + if( nIndex ) + aIndex += nIndex; + maPolygons.insert(aIndex, nCount, rPolygon); + } + } + + void insert(sal_uInt32 nIndex, const ::basegfx::B3DPolyPolygon& rPolyPolygon) + { + // add all polygons from rPolyPolygon + PolygonVector::iterator aIndex(maPolygons.begin()); + if( nIndex ) + aIndex += nIndex; + maPolygons.insert(aIndex, rPolyPolygon.begin(), rPolyPolygon.end()); + } + + void remove(sal_uInt32 nIndex, sal_uInt32 nCount) + { + if(nCount) + { + // remove polygon data + PolygonVector::iterator aStart(maPolygons.begin()); + aStart += nIndex; + const PolygonVector::iterator aEnd(aStart + nCount); + + maPolygons.erase(aStart, aEnd); + } + } + + sal_uInt32 count() const + { + return maPolygons.size(); + } + + void flip() + { + for (auto& aPolygon : maPolygons) + aPolygon.flip(); + } + + void removeDoublePoints() + { + for (auto& aPolygon : maPolygons) + aPolygon.removeDoublePoints(); + } + + void transform(const ::basegfx::B3DHomMatrix& rMatrix) + { + for (auto& aPolygon : maPolygons) + aPolygon.transform(rMatrix); + } + + void clearBColors() + { + for (auto& aPolygon : maPolygons) + aPolygon.clearBColors(); + } + + void transformNormals(const ::basegfx::B3DHomMatrix& rMatrix) + { + for (auto& aPolygon : maPolygons) + aPolygon.transformNormals(rMatrix); + } + + void clearNormals() + { + for (auto& aPolygon : maPolygons) + aPolygon.clearNormals(); + } + + void transformTextureCoordinates(const ::basegfx::B2DHomMatrix& rMatrix) + { + for (auto& aPolygon : maPolygons) + aPolygon.transformTextureCoordinates(rMatrix); + } + + void clearTextureCoordinates() + { + for (auto& aPolygon : maPolygons) + aPolygon.clearTextureCoordinates(); + } + + const basegfx::B3DPolygon* begin() const + { + if (maPolygons.empty()) + return nullptr; + else + return maPolygons.data(); + } + + const basegfx::B3DPolygon* end() const + { + if (maPolygons.empty()) + return nullptr; + else + return maPolygons.data() + maPolygons.size(); + } + + basegfx::B3DPolygon* begin() + { + if (maPolygons.empty()) + return nullptr; + else + return maPolygons.data(); + } + + basegfx::B3DPolygon* end() + { + if (maPolygons.empty()) + return nullptr; + else + return maPolygons.data() + maPolygons.size(); + } +}; + +namespace basegfx +{ + namespace { + + B3DPolyPolygon::ImplType const & getDefaultPolyPolygon() { + static B3DPolyPolygon::ImplType const singleton; + return singleton; + } + + } + + B3DPolyPolygon::B3DPolyPolygon() : + mpPolyPolygon(getDefaultPolyPolygon()) + { + } + + B3DPolyPolygon::B3DPolyPolygon(const B3DPolyPolygon&) = default; + + B3DPolyPolygon::B3DPolyPolygon(B3DPolyPolygon&&) = default; + + B3DPolyPolygon::B3DPolyPolygon(const B3DPolygon& rPolygon) : + mpPolyPolygon( ImplB3DPolyPolygon(rPolygon) ) + { + } + + B3DPolyPolygon::~B3DPolyPolygon() = default; + + B3DPolyPolygon& B3DPolyPolygon::operator=(const B3DPolyPolygon&) = default; + + B3DPolyPolygon& B3DPolyPolygon::operator=(B3DPolyPolygon&&) = default; + + bool B3DPolyPolygon::operator==(const B3DPolyPolygon& rPolyPolygon) const + { + if(mpPolyPolygon.same_object(rPolyPolygon.mpPolyPolygon)) + return true; + + return ((*mpPolyPolygon) == (*rPolyPolygon.mpPolyPolygon)); + } + + bool B3DPolyPolygon::operator!=(const B3DPolyPolygon& rPolyPolygon) const + { + return !(*this == rPolyPolygon); + } + + sal_uInt32 B3DPolyPolygon::count() const + { + return mpPolyPolygon->count(); + } + + B3DPolygon const & B3DPolyPolygon::getB3DPolygon(sal_uInt32 nIndex) const + { + OSL_ENSURE(nIndex < mpPolyPolygon->count(), "B3DPolyPolygon access outside range (!)"); + + return mpPolyPolygon->getB3DPolygon(nIndex); + } + + void B3DPolyPolygon::setB3DPolygon(sal_uInt32 nIndex, const B3DPolygon& rPolygon) + { + OSL_ENSURE(nIndex < std::as_const(mpPolyPolygon)->count(), "B3DPolyPolygon access outside range (!)"); + + if(getB3DPolygon(nIndex) != rPolygon) + mpPolyPolygon->setB3DPolygon(nIndex, rPolygon); + } + + bool B3DPolyPolygon::areBColorsUsed() const + { + for(sal_uInt32 a(0); a < mpPolyPolygon->count(); a++) + { + if(mpPolyPolygon->getB3DPolygon(a).areBColorsUsed()) + { + return true; + } + } + + return false; + } + + void B3DPolyPolygon::clearBColors() + { + if(areBColorsUsed()) + mpPolyPolygon->clearBColors(); + } + + void B3DPolyPolygon::transformNormals(const B3DHomMatrix& rMatrix) + { + if(!rMatrix.isIdentity()) + mpPolyPolygon->transformNormals(rMatrix); + } + + bool B3DPolyPolygon::areNormalsUsed() const + { + for(sal_uInt32 a(0); a < mpPolyPolygon->count(); a++) + { + if(mpPolyPolygon->getB3DPolygon(a).areNormalsUsed()) + { + return true; + } + } + + return false; + } + + void B3DPolyPolygon::clearNormals() + { + if(areNormalsUsed()) + mpPolyPolygon->clearNormals(); + } + + void B3DPolyPolygon::transformTextureCoordinates(const B2DHomMatrix& rMatrix) + { + if(!rMatrix.isIdentity()) + mpPolyPolygon->transformTextureCoordinates(rMatrix); + } + + bool B3DPolyPolygon::areTextureCoordinatesUsed() const + { + for(sal_uInt32 a(0); a < mpPolyPolygon->count(); a++) + { + if(mpPolyPolygon->getB3DPolygon(a).areTextureCoordinatesUsed()) + { + return true; + } + } + + return false; + } + + void B3DPolyPolygon::clearTextureCoordinates() + { + if(areTextureCoordinatesUsed()) + mpPolyPolygon->clearTextureCoordinates(); + } + + void B3DPolyPolygon::append(const B3DPolygon& rPolygon, sal_uInt32 nCount) + { + if(nCount) + mpPolyPolygon->insert(std::as_const(mpPolyPolygon)->count(), rPolygon, nCount); + } + + void B3DPolyPolygon::append(const B3DPolyPolygon& rPolyPolygon) + { + if(rPolyPolygon.count()) + mpPolyPolygon->insert(std::as_const(mpPolyPolygon)->count(), rPolyPolygon); + } + + void B3DPolyPolygon::remove(sal_uInt32 nIndex, sal_uInt32 nCount) + { + OSL_ENSURE(nIndex + nCount <= std::as_const(mpPolyPolygon)->count(), "B3DPolyPolygon Remove outside range (!)"); + + if(nCount) + mpPolyPolygon->remove(nIndex, nCount); + } + + void B3DPolyPolygon::clear() + { + mpPolyPolygon = getDefaultPolyPolygon(); + } + + void B3DPolyPolygon::flip() + { + mpPolyPolygon->flip(); + } + + bool B3DPolyPolygon::hasDoublePoints() const + { + bool bRetval(false); + + for(sal_uInt32 a(0); !bRetval && a < mpPolyPolygon->count(); a++) + { + if(mpPolyPolygon->getB3DPolygon(a).hasDoublePoints()) + { + bRetval = true; + } + } + + return bRetval; + } + + void B3DPolyPolygon::removeDoublePoints() + { + if(hasDoublePoints()) + mpPolyPolygon->removeDoublePoints(); + } + + void B3DPolyPolygon::transform(const B3DHomMatrix& rMatrix) + { + if(std::as_const(mpPolyPolygon)->count() && !rMatrix.isIdentity()) + { + mpPolyPolygon->transform(rMatrix); + } + } + + const B3DPolygon* B3DPolyPolygon::begin() const + { + return mpPolyPolygon->begin(); + } + + const B3DPolygon* B3DPolyPolygon::end() const + { + return mpPolyPolygon->end(); + } + + B3DPolygon* B3DPolyPolygon::begin() + { + return mpPolyPolygon->begin(); + } + + B3DPolygon* B3DPolyPolygon::end() + { + return mpPolyPolygon->end(); + } +} // end of namespace basegfx + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/basegfx/source/polygon/b3dpolypolygontools.cxx b/basegfx/source/polygon/b3dpolypolygontools.cxx new file mode 100644 index 0000000000..ffd8bded4d --- /dev/null +++ b/basegfx/source/polygon/b3dpolypolygontools.cxx @@ -0,0 +1,585 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * 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 . + */ + +#include <basegfx/polygon/b3dpolypolygontools.hxx> +#include <basegfx/range/b3drange.hxx> +#include <basegfx/polygon/b3dpolypolygon.hxx> +#include <basegfx/polygon/b3dpolygon.hxx> +#include <basegfx/polygon/b3dpolygontools.hxx> +#include <basegfx/matrix/b3dhommatrix.hxx> +#include <basegfx/numeric/ftools.hxx> +#include <com/sun/star/drawing/DoubleSequence.hpp> +#include <com/sun/star/drawing/PolyPolygonShape3D.hpp> + +// predefines +#define nMinSegments sal_uInt32(1) +#define nMaxSegments sal_uInt32(512) + +namespace basegfx::utils +{ + // B3DPolyPolygon tools + B3DRange getRange(const B3DPolyPolygon& rCandidate) + { + B3DRange aRetval; + + for(const auto& rPolygon : rCandidate ) + { + aRetval.expand(getRange(rPolygon)); + } + + return aRetval; + } + + B3DPolyPolygon const & createUnitCubePolyPolygon() + { + static auto const singleton = [] { + B3DPolyPolygon aRetval; + B3DPolygon aTemp; + aTemp.append(B3DPoint(0.0, 0.0, 1.0)); + aTemp.append(B3DPoint(0.0, 1.0, 1.0)); + aTemp.append(B3DPoint(1.0, 1.0, 1.0)); + aTemp.append(B3DPoint(1.0, 0.0, 1.0)); + aTemp.setClosed(true); + aRetval.append(aTemp); + + aTemp.clear(); + aTemp.append(B3DPoint(0.0, 0.0, 0.0)); + aTemp.append(B3DPoint(0.0, 1.0, 0.0)); + aTemp.append(B3DPoint(1.0, 1.0, 0.0)); + aTemp.append(B3DPoint(1.0, 0.0, 0.0)); + aTemp.setClosed(true); + aRetval.append(aTemp); + + aTemp.clear(); + aTemp.append(B3DPoint(0.0, 0.0, 0.0)); + aTemp.append(B3DPoint(0.0, 0.0, 1.0)); + aRetval.append(aTemp); + + aTemp.clear(); + aTemp.append(B3DPoint(0.0, 1.0, 0.0)); + aTemp.append(B3DPoint(0.0, 1.0, 1.0)); + aRetval.append(aTemp); + + aTemp.clear(); + aTemp.append(B3DPoint(1.0, 1.0, 0.0)); + aTemp.append(B3DPoint(1.0, 1.0, 1.0)); + aRetval.append(aTemp); + + aTemp.clear(); + aTemp.append(B3DPoint(1.0, 0.0, 0.0)); + aTemp.append(B3DPoint(1.0, 0.0, 1.0)); + aRetval.append(aTemp); + return aRetval; + }(); + return singleton; + } + + B3DPolyPolygon const & createUnitCubeFillPolyPolygon() + { + static auto const singleton = [] { + B3DPolyPolygon aRetval; + B3DPolygon aTemp; + + // all points + const B3DPoint A(0.0, 0.0, 0.0); + const B3DPoint B(0.0, 1.0, 0.0); + const B3DPoint C(1.0, 1.0, 0.0); + const B3DPoint D(1.0, 0.0, 0.0); + const B3DPoint E(0.0, 0.0, 1.0); + const B3DPoint F(0.0, 1.0, 1.0); + const B3DPoint G(1.0, 1.0, 1.0); + const B3DPoint H(1.0, 0.0, 1.0); + + // create bottom + aTemp.append(D); + aTemp.append(A); + aTemp.append(E); + aTemp.append(H); + aTemp.setClosed(true); + aRetval.append(aTemp); + + // create front + aTemp.clear(); + aTemp.append(B); + aTemp.append(A); + aTemp.append(D); + aTemp.append(C); + aTemp.setClosed(true); + aRetval.append(aTemp); + + // create left + aTemp.clear(); + aTemp.append(E); + aTemp.append(A); + aTemp.append(B); + aTemp.append(F); + aTemp.setClosed(true); + aRetval.append(aTemp); + + // create top + aTemp.clear(); + aTemp.append(C); + aTemp.append(G); + aTemp.append(F); + aTemp.append(B); + aTemp.setClosed(true); + aRetval.append(aTemp); + + // create right + aTemp.clear(); + aTemp.append(H); + aTemp.append(G); + aTemp.append(C); + aTemp.append(D); + aTemp.setClosed(true); + aRetval.append(aTemp); + + // create back + aTemp.clear(); + aTemp.append(F); + aTemp.append(G); + aTemp.append(H); + aTemp.append(E); + aTemp.setClosed(true); + aRetval.append(aTemp); + return aRetval; + }(); + return singleton; + } + + B3DPolyPolygon createCubePolyPolygonFromB3DRange( const B3DRange& rRange) + { + B3DPolyPolygon aRetval; + + if(!rRange.isEmpty()) + { + aRetval = createUnitCubePolyPolygon(); + B3DHomMatrix aTrans; + aTrans.scale(rRange.getWidth(), rRange.getHeight(), rRange.getDepth()); + aTrans.translate(rRange.getMinX(), rRange.getMinY(), rRange.getMinZ()); + aRetval.transform(aTrans); + aRetval.removeDoublePoints(); + } + + return aRetval; + } + + B3DPolyPolygon createCubeFillPolyPolygonFromB3DRange( const B3DRange& rRange) + { + B3DPolyPolygon aRetval; + + if(!rRange.isEmpty()) + { + aRetval = createUnitCubeFillPolyPolygon(); + B3DHomMatrix aTrans; + aTrans.scale(rRange.getWidth(), rRange.getHeight(), rRange.getDepth()); + aTrans.translate(rRange.getMinX(), rRange.getMinY(), rRange.getMinZ()); + aRetval.transform(aTrans); + aRetval.removeDoublePoints(); + } + + return aRetval; + } + + // helper for getting the 3D Point from given cartesian coordinates. fHor is defined from + // [M_PI_2 .. -M_PI_2], fVer from [0.0 .. 2PI] + static B3DPoint getPointFromCartesian(double fHor, double fVer) + { + const double fCosVer(cos(fVer)); + return B3DPoint(fCosVer * cos(fHor), sin(fVer), fCosVer * -sin(fHor)); + } + + B3DPolyPolygon createUnitSpherePolyPolygon( + sal_uInt32 nHorSeg, sal_uInt32 nVerSeg, + double fVerStart, double fVerStop, + double fHorStart, double fHorStop) + { + B3DPolyPolygon aRetval; + sal_uInt32 a, b; + + if(!nHorSeg) + { + nHorSeg = fround(fabs(fHorStop - fHorStart) / (M_PI / 12.0)); + } + + // min/max limitations + nHorSeg = std::clamp(nHorSeg, nMinSegments, nMaxSegments); + + if(!nVerSeg) + { + nVerSeg = fround(fabs(fVerStop - fVerStart) / (M_PI / 12.0)); + } + + // min/max limitations + nVerSeg = std::clamp(nVerSeg, nMinSegments, nMaxSegments); + + // create constants + const double fVerDiffPerStep((fVerStop - fVerStart) / static_cast<double>(nVerSeg)); + const double fHorDiffPerStep((fHorStop - fHorStart) / static_cast<double>(nHorSeg)); + bool bHorClosed(fTools::equal(fHorStop - fHorStart, 2 * M_PI)); + bool bVerFromTop(fTools::equal(fVerStart, M_PI_2)); + bool bVerToBottom(fTools::equal(fVerStop, -M_PI_2)); + + // create horizontal rings + const sal_uInt32 nLoopVerInit(bVerFromTop ? 1 : 0); + const sal_uInt32 nLoopVerLimit(bVerToBottom ? nVerSeg : nVerSeg + 1); + const sal_uInt32 nLoopHorLimit(bHorClosed ? nHorSeg : nHorSeg + 1); + + for(a = nLoopVerInit; a < nLoopVerLimit; a++) + { + const double fVer(fVerStart + (static_cast<double>(a) * fVerDiffPerStep)); + B3DPolygon aNew; + + for(b = 0; b < nLoopHorLimit; b++) + { + const double fHor(fHorStart + (static_cast<double>(b) * fHorDiffPerStep)); + aNew.append(getPointFromCartesian(fHor, fVer)); + } + + aNew.setClosed(bHorClosed); + aRetval.append(aNew); + } + + // create vertical half-rings + for(a = 0; a < nLoopHorLimit; a++) + { + const double fHor(fHorStart + (static_cast<double>(a) * fHorDiffPerStep)); + B3DPolygon aNew; + + if(bVerFromTop) + { + aNew.append(B3DPoint(0.0, 1.0, 0.0)); + } + + for(b = nLoopVerInit; b < nLoopVerLimit; b++) + { + const double fVer(fVerStart + (static_cast<double>(b) * fVerDiffPerStep)); + aNew.append(getPointFromCartesian(fHor, fVer)); + } + + if(bVerToBottom) + { + aNew.append(B3DPoint(0.0, -1.0, 0.0)); + } + + aRetval.append(aNew); + } + + return aRetval; + } + + B3DPolyPolygon createSpherePolyPolygonFromB3DRange( const B3DRange& rRange, + sal_uInt32 nHorSeg, sal_uInt32 nVerSeg, + double fVerStart, double fVerStop, + double fHorStart, double fHorStop) + { + B3DPolyPolygon aRetval(createUnitSpherePolyPolygon(nHorSeg, nVerSeg, fVerStart, fVerStop, fHorStart, fHorStop)); + + if(aRetval.count()) + { + // move and scale whole construct which is now in [-1.0 .. 1.0] in all directions + B3DHomMatrix aTrans; + aTrans.translate(1.0, 1.0, 1.0); + aTrans.scale(rRange.getWidth() / 2.0, rRange.getHeight() / 2.0, rRange.getDepth() / 2.0); + aTrans.translate(rRange.getMinX(), rRange.getMinY(), rRange.getMinZ()); + aRetval.transform(aTrans); + } + + return aRetval; + } + + B3DPolyPolygon createUnitSphereFillPolyPolygon( + sal_uInt32 nHorSeg, sal_uInt32 nVerSeg, + bool bNormals, + double fVerStart, double fVerStop, + double fHorStart, double fHorStop) + { + B3DPolyPolygon aRetval; + + if(!nHorSeg) + { + nHorSeg = fround(fabs(fHorStop - fHorStart) / (M_PI / 12.0)); + } + + // min/max limitations + nHorSeg = std::clamp(nHorSeg, nMinSegments, nMaxSegments); + + if(!nVerSeg) + { + nVerSeg = fround(fabs(fVerStop - fVerStart) / (M_PI / 12.0)); + } + + // min/max limitations + nVerSeg = std::clamp(nVerSeg, nMinSegments, nMaxSegments); + + // vertical loop + for(sal_uInt32 a(0); a < nVerSeg; a++) + { + const double fVer1(fVerStart + (((fVerStop - fVerStart) * a) / nVerSeg)); + const double fVer2(fVerStart + (((fVerStop - fVerStart) * (a + 1)) / nVerSeg)); + + // horizontal loop + for(sal_uInt32 b(0); b < nHorSeg; b++) + { + const double fHor1(fHorStart + (((fHorStop - fHorStart) * b) / nHorSeg)); + const double fHor2(fHorStart + (((fHorStop - fHorStart) * (b + 1)) / nHorSeg)); + B3DPolygon aNew; + + aNew.append(getPointFromCartesian(fHor1, fVer1)); + aNew.append(getPointFromCartesian(fHor2, fVer1)); + aNew.append(getPointFromCartesian(fHor2, fVer2)); + aNew.append(getPointFromCartesian(fHor1, fVer2)); + + if(bNormals) + { + for(sal_uInt32 c(0); c < aNew.count(); c++) + { + aNew.setNormal(c, ::basegfx::B3DVector(aNew.getB3DPoint(c))); + } + } + + aNew.setClosed(true); + aRetval.append(aNew); + } + } + + return aRetval; + } + + B3DPolyPolygon createSphereFillPolyPolygonFromB3DRange( const B3DRange& rRange, + sal_uInt32 nHorSeg, sal_uInt32 nVerSeg, + bool bNormals, + double fVerStart, double fVerStop, + double fHorStart, double fHorStop) + { + B3DPolyPolygon aRetval(createUnitSphereFillPolyPolygon(nHorSeg, nVerSeg, bNormals, fVerStart, fVerStop, fHorStart, fHorStop)); + + if(aRetval.count()) + { + // move and scale whole construct which is now in [-1.0 .. 1.0] in all directions + B3DHomMatrix aTrans; + aTrans.translate(1.0, 1.0, 1.0); + aTrans.scale(rRange.getWidth() / 2.0, rRange.getHeight() / 2.0, rRange.getDepth() / 2.0); + aTrans.translate(rRange.getMinX(), rRange.getMinY(), rRange.getMinZ()); + aRetval.transform(aTrans); + } + + return aRetval; + } + + B3DPolyPolygon applyDefaultNormalsSphere( const B3DPolyPolygon& rCandidate, const B3DPoint& rCenter) + { + B3DPolyPolygon aRetval; + + for( const auto& rB3DPolygon : rCandidate) + { + aRetval.append(applyDefaultNormalsSphere(rB3DPolygon, rCenter)); + } + + return aRetval; + } + + B3DPolyPolygon invertNormals( const B3DPolyPolygon& rCandidate) + { + B3DPolyPolygon aRetval; + + for( const auto& rB3DPolygon : rCandidate ) + { + aRetval.append(invertNormals(rB3DPolygon)); + } + + return aRetval; + } + + B3DPolyPolygon applyDefaultTextureCoordinatesParallel( const B3DPolyPolygon& rCandidate, const B3DRange& rRange, bool bChangeX, bool bChangeY) + { + B3DPolyPolygon aRetval; + + for( const auto& rB3DPolygon : rCandidate) + { + aRetval.append(applyDefaultTextureCoordinatesParallel(rB3DPolygon, rRange, bChangeX, bChangeY)); + } + + return aRetval; + } + + B3DPolyPolygon applyDefaultTextureCoordinatesSphere( const B3DPolyPolygon& rCandidate, const B3DPoint& rCenter, bool bChangeX, bool bChangeY) + { + B3DPolyPolygon aRetval; + + for( const auto& rB3DPolygon : rCandidate ) + { + aRetval.append(applyDefaultTextureCoordinatesSphere(rB3DPolygon, rCenter, bChangeX, bChangeY)); + } + + return aRetval; + } + + bool isInside(const B3DPolyPolygon& rCandidate, const B3DPoint& rPoint) + { + const sal_uInt32 nPolygonCount(rCandidate.count()); + + if(nPolygonCount == 1) + { + return isInside(rCandidate.getB3DPolygon(0), rPoint, false/*bWithBorder*/); + } + else + { + sal_Int32 nInsideCount(0); + + for(const auto& rPolygon : rCandidate ) + { + const bool bInside(isInside(rPolygon, rPoint, false/*bWithBorder*/)); + + if(bInside) + { + nInsideCount++; + } + } + + return (nInsideCount % 2); + } + } + +/// converters for css::drawing::PolyPolygonShape3D + B3DPolyPolygon UnoPolyPolygonShape3DToB3DPolyPolygon( + const css::drawing::PolyPolygonShape3D& rPolyPolygonShape3DSource) + { + B3DPolyPolygon aRetval; + const sal_Int32 nOuterSequenceCount(rPolyPolygonShape3DSource.SequenceX.getLength()); + + if(nOuterSequenceCount) + { + assert(nOuterSequenceCount == rPolyPolygonShape3DSource.SequenceY.getLength() + && nOuterSequenceCount + == rPolyPolygonShape3DSource.SequenceZ.getLength()&& + "UnoPolyPolygonShape3DToB3DPolygon: Not all double sequences have the same " + "length (!)" ); + + const css::drawing::DoubleSequence* pInnerSequenceX = rPolyPolygonShape3DSource.SequenceX.getConstArray(); + const css::drawing::DoubleSequence* pInnerSequenceY = rPolyPolygonShape3DSource.SequenceY.getConstArray(); + const css::drawing::DoubleSequence* pInnerSequenceZ = rPolyPolygonShape3DSource.SequenceZ.getConstArray(); + + for(sal_Int32 a(0); a < nOuterSequenceCount; a++) + { + basegfx::B3DPolygon aNewPolygon; + const sal_Int32 nInnerSequenceCount(pInnerSequenceX->getLength()); + assert(nInnerSequenceCount == pInnerSequenceY->getLength() + && nInnerSequenceCount == pInnerSequenceZ->getLength() + && "UnoPolyPolygonShape3DToB3DPolygon: Not all double sequences have " + "the same length (!)"); + + const double* pArrayX = pInnerSequenceX->getConstArray(); + const double* pArrayY = pInnerSequenceY->getConstArray(); + const double* pArrayZ = pInnerSequenceZ->getConstArray(); + + for(sal_Int32 b(0); b < nInnerSequenceCount; b++) + { + aNewPolygon.append(basegfx::B3DPoint(*pArrayX++,*pArrayY++,*pArrayZ++)); + } + + pInnerSequenceX++; + pInnerSequenceY++; + pInnerSequenceZ++; + + // #i101520# correction is needed for imported polygons of old format, + // see callers + basegfx::utils::checkClosed(aNewPolygon); + + aRetval.append(aNewPolygon); + } + } + + return aRetval; + } + + void B3DPolyPolygonToUnoPolyPolygonShape3D( + const B3DPolyPolygon& rPolyPolygonSource, + css::drawing::PolyPolygonShape3D& rPolyPolygonShape3DRetval) + { + const sal_uInt32 nPolygonCount(rPolyPolygonSource.count()); + + if(nPolygonCount) + { + rPolyPolygonShape3DRetval.SequenceX.realloc(nPolygonCount); + rPolyPolygonShape3DRetval.SequenceY.realloc(nPolygonCount); + rPolyPolygonShape3DRetval.SequenceZ.realloc(nPolygonCount); + + css::drawing::DoubleSequence* pOuterSequenceX = rPolyPolygonShape3DRetval.SequenceX.getArray(); + css::drawing::DoubleSequence* pOuterSequenceY = rPolyPolygonShape3DRetval.SequenceY.getArray(); + css::drawing::DoubleSequence* pOuterSequenceZ = rPolyPolygonShape3DRetval.SequenceZ.getArray(); + + for(sal_uInt32 a(0); a < nPolygonCount; a++) + { + const basegfx::B3DPolygon& aPoly(rPolyPolygonSource.getB3DPolygon(a)); + const sal_uInt32 nPointCount(aPoly.count()); + + if(nPointCount) + { + const bool bIsClosed(aPoly.isClosed()); + const sal_uInt32 nTargetCount(bIsClosed ? nPointCount + 1 : nPointCount); + pOuterSequenceX->realloc(nTargetCount); + pOuterSequenceY->realloc(nTargetCount); + pOuterSequenceZ->realloc(nTargetCount); + + double* pInnerSequenceX = pOuterSequenceX->getArray(); + double* pInnerSequenceY = pOuterSequenceY->getArray(); + double* pInnerSequenceZ = pOuterSequenceZ->getArray(); + + for(sal_uInt32 b(0); b < nPointCount; b++) + { + const basegfx::B3DPoint aPoint(aPoly.getB3DPoint(b)); + + *pInnerSequenceX++ = aPoint.getX(); + *pInnerSequenceY++ = aPoint.getY(); + *pInnerSequenceZ++ = aPoint.getZ(); + } + + if(bIsClosed) + { + const basegfx::B3DPoint aPoint(aPoly.getB3DPoint(0)); + + *pInnerSequenceX++ = aPoint.getX(); + *pInnerSequenceY++ = aPoint.getY(); + *pInnerSequenceZ++ = aPoint.getZ(); + } + } + else + { + pOuterSequenceX->realloc(0); + pOuterSequenceY->realloc(0); + pOuterSequenceZ->realloc(0); + } + + pOuterSequenceX++; + pOuterSequenceY++; + pOuterSequenceZ++; + } + } + else + { + rPolyPolygonShape3DRetval.SequenceX.realloc(0); + rPolyPolygonShape3DRetval.SequenceY.realloc(0); + rPolyPolygonShape3DRetval.SequenceZ.realloc(0); + } + } + +} // end of namespace + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/basegfx/source/range/b2dpolyrange.cxx b/basegfx/source/range/b2dpolyrange.cxx new file mode 100644 index 0000000000..beb506fc8e --- /dev/null +++ b/basegfx/source/range/b2dpolyrange.cxx @@ -0,0 +1,153 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * 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 . + */ + +#include <basegfx/range/b2dpolyrange.hxx> + +#include <basegfx/range/b2drange.hxx> +#include <basegfx/range/b2drangeclipper.hxx> +#include <basegfx/polygon/b2dpolypolygon.hxx> + +#include <algorithm> +#include <vector> + +namespace basegfx +{ + class ImplB2DPolyRange + { + public: + ImplB2DPolyRange() + {} + + bool operator==(const ImplB2DPolyRange& rRHS) const + { + return maRanges == rRHS.maRanges && maOrient == rRHS.maOrient; + } + + sal_uInt32 count() const + { + return maRanges.size(); + } + + B2DPolyRange::ElementType getElement(sal_uInt32 nIndex) const + { + return std::make_tuple(maRanges[nIndex], maOrient[nIndex]); + } + + void appendElement(const B2DRange& rRange, B2VectorOrientation eOrient) + { + maRanges.push_back(rRange); + maOrient.push_back(eOrient); + maBounds.expand(rRange); + } + + void clear() + { + std::vector<B2DRange>().swap(maRanges); + std::vector<B2VectorOrientation>().swap(maOrient); + + maBounds.reset(); + } + + bool overlaps( const B2DRange& rRange ) const + { + if( !maBounds.overlaps( rRange ) ) + return false; + + const std::vector<B2DRange>::const_iterator aEnd( maRanges.end() ); + return std::any_of( maRanges.begin(), + aEnd, + [&rRange](const B2DRange& aRange) { return aRange.overlaps(rRange); } ); + } + + B2DPolyPolygon solveCrossovers() const + { + return utils::solveCrossovers(maRanges,maOrient); + } + + void transform(const basegfx::B2DHomMatrix& rTranslate) + { + maBounds.transform(rTranslate); + for (auto &a : maRanges) + a.transform(rTranslate); + } + + private: + B2DRange maBounds; + std::vector<B2DRange> maRanges; + std::vector<B2VectorOrientation> maOrient; + }; + + B2DPolyRange::B2DPolyRange() = default; + + B2DPolyRange::~B2DPolyRange() = default; + + B2DPolyRange::B2DPolyRange( const B2DPolyRange& ) = default; + + B2DPolyRange& B2DPolyRange::operator=( const B2DPolyRange& ) = default; + + bool B2DPolyRange::operator==(const B2DPolyRange& rRange) const + { + if(mpImpl.same_object(rRange.mpImpl)) + return true; + + return ((*mpImpl) == (*rRange.mpImpl)); + } + + bool B2DPolyRange::operator!=(const B2DPolyRange& rRange) const + { + return !(*this == rRange); + } + + sal_uInt32 B2DPolyRange::count() const + { + return mpImpl->count(); + } + + B2DPolyRange::ElementType B2DPolyRange::getElement(sal_uInt32 nIndex) const + { + return mpImpl->getElement(nIndex); + } + + void B2DPolyRange::appendElement(const B2DRange& rRange, B2VectorOrientation eOrient) + { + mpImpl->appendElement(rRange, eOrient); + } + + void B2DPolyRange::clear() + { + mpImpl->clear(); + } + + bool B2DPolyRange::overlaps( const B2DRange& rRange ) const + { + return mpImpl->overlaps(rRange); + } + + void B2DPolyRange::transform(const basegfx::B2DHomMatrix& rTranslate) + { + mpImpl->transform(rTranslate); + } + + B2DPolyPolygon B2DPolyRange::solveCrossovers() const + { + return mpImpl->solveCrossovers(); + } +} // end of namespace basegfx + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/basegfx/source/range/b2drange.cxx b/basegfx/source/range/b2drange.cxx new file mode 100644 index 0000000000..1f20cce90a --- /dev/null +++ b/basegfx/source/range/b2drange.cxx @@ -0,0 +1,81 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * 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 . + */ + +#include <basegfx/range/b2drange.hxx> +#include <basegfx/range/b2irange.hxx> +#include <basegfx/matrix/b2dhommatrix.hxx> + +namespace basegfx +{ + B2DRange::B2DRange(const B2IRange& rRange) + { + if (!rRange.isEmpty()) + { + maRangeX = basegfx::BasicRange<ValueType, TraitsType>(rRange.getMinX()); + maRangeY = basegfx::BasicRange<ValueType, TraitsType>(rRange.getMinY()); + + maRangeX.expand(rRange.getMaxX()); + maRangeY.expand(rRange.getMaxY()); + } + } + + void B2DRange::transform(const B2DHomMatrix& rMatrix) + { + if(!isEmpty() && !rMatrix.isIdentity()) + { + const B2DRange aSource(*this); + reset(); + expand(rMatrix * B2DPoint(aSource.getMinX(), aSource.getMinY())); + expand(rMatrix * B2DPoint(aSource.getMaxX(), aSource.getMinY())); + expand(rMatrix * B2DPoint(aSource.getMinX(), aSource.getMaxY())); + expand(rMatrix * B2DPoint(aSource.getMaxX(), aSource.getMaxY())); + } + } + + B2DRange& B2DRange::operator*=(const basegfx::B2DHomMatrix& rMatrix) + { + transform(rMatrix); + return *this; + } + + const B2DRange& B2DRange::getUnitB2DRange() + { + static const B2DRange aUnitB2DRange(0.0, 0.0, 1.0, 1.0); + + return aUnitB2DRange; + } + + B2IRange fround(const B2DRange& rRange) + { + return rRange.isEmpty() ? + B2IRange() : + B2IRange(fround(rRange.getMinimum()), + fround(rRange.getMaximum())); + } + + B2DRange operator*( const ::basegfx::B2DHomMatrix& rMat, const B2DRange& rB2DRange ) + { + B2DRange aRes( rB2DRange ); + aRes *= rMat; + return aRes; + } + +} // end of namespace basegfx + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/basegfx/source/range/b2drangeclipper.cxx b/basegfx/source/range/b2drangeclipper.cxx new file mode 100644 index 0000000000..401ff20caa --- /dev/null +++ b/basegfx/source/range/b2drangeclipper.cxx @@ -0,0 +1,877 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * 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 . + */ + +#include <osl/diagnose.h> + +#include <basegfx/range/b2drange.hxx> +#include <basegfx/range/b2drangeclipper.hxx> +#include <basegfx/polygon/b2dpolypolygon.hxx> +#include <basegfx/range/b2drectangle.hxx> + +#include <o3tl/vector_pool.hxx> + +#include <algorithm> +#include <cassert> +#include <list> +#include <iterator> + +namespace basegfx +{ + namespace + { + // Generating a poly-polygon from a bunch of rectangles + + // Helper functionality for sweep-line algorithm + // ==================================================== + + class ImplPolygon; + typedef o3tl::vector_pool<ImplPolygon> VectorOfPolygons; + + /** This class represents an active edge + + As the sweep line traverses across the overall area, + rectangle edges parallel to it generate events, and + rectangle edges orthogonal to it generate active + edges. This class represents the latter. + */ + class ActiveEdge + { + public: + + enum EdgeDirection { + /// edge proceeds to the left + PROCEED_LEFT=0, + /// edge proceeds to the right + PROCEED_RIGHT=1 + }; + + /** Create active edge + + @param rRect + Rectangle this edge is part of + + @param fInvariantCoord + The invariant coordinate value of this edge + + @param eEdgeType + Is fInvariantCoord the lower or the higher value, for + this rect? + */ + ActiveEdge( const B2DRectangle& rRect, + const double& fInvariantCoord, + std::ptrdiff_t nPolyIdx, + EdgeDirection eEdgeDirection ) : + mfInvariantCoord(fInvariantCoord), + mpAssociatedRect( &rRect ), + mnPolygonIdx( nPolyIdx ), + meEdgeDirection( eEdgeDirection ) + {} + + double getInvariantCoord() const { return mfInvariantCoord; } + const B2DRectangle& getRect() const { return *mpAssociatedRect; } + std::ptrdiff_t getTargetPolygonIndex() const { return mnPolygonIdx; } + void setTargetPolygonIndex( std::ptrdiff_t nIdx ) { mnPolygonIdx = nIdx; } + EdgeDirection getEdgeDirection() const { return meEdgeDirection; } + + private: + /** The invariant coordinate value of this edge (e.g. the + common y value, for a horizontal edge) + */ + double mfInvariantCoord; + + /** Associated rectangle + + This on the one hand saves some storage space (the + vector of rectangles is persistent, anyway), and on + the other hand provides an identifier to match active + edges and x events (see below) + + Ptr because class needs to be assignable + */ + const B2DRectangle* mpAssociatedRect; + + /** Index of the polygon this edge is currently involved + with. + + Note that this can change for some kinds of edge + intersection, as the algorithm tends to swap + associated polygons there. + + -1 denotes no assigned polygon + */ + std::ptrdiff_t mnPolygonIdx; + + /// 'left' or 'right' + EdgeDirection meEdgeDirection; + }; + + // Needs to be list - various places hold ptrs to elements + typedef std::list< ActiveEdge > ListOfEdges; + + /** Element of the sweep line event list + + As the sweep line traverses across the overall area, + rectangle edges parallel to it generate events, and + rectangle edges orthogonal to it generate active + edges. This class represents the former. + + The class defines an element of the sweep line list. The + sweep line's position jumps in steps defined by the + coordinates of the sorted SweepLineEvent entries. + */ + class SweepLineEvent + { + public: + /** The two possible sweep line rectangle edges differ by + one coordinate value - the starting edge has the + lower, the finishing edge the higher value. + */ + enum EdgeType { + /// edge with lower coordinate value + STARTING_EDGE=0, + /// edge with higher coordinate value + FINISHING_EDGE=1 + }; + + /** The two possible sweep line directions + */ + enum EdgeDirection { + PROCEED_UP=0, + PROCEED_DOWN=1 + }; + + /** Create sweep line event + + @param fPos + Coordinate position of the event + + @param rRect + Rectangle this event is generated for. + + @param eEdgeType + Is fPos the lower or the higher value, for the + rectangle this event is generated for? + */ + SweepLineEvent( double fPos, + const B2DRectangle& rRect, + EdgeType eEdgeType, + EdgeDirection eDirection) : + mfPos( fPos ), + mpAssociatedRect( &rRect ), + meEdgeType( eEdgeType ), + meEdgeDirection( eDirection ) + {} + + double getPos() const { return mfPos; } + const B2DRectangle& getRect() const { return *mpAssociatedRect; } + EdgeType getEdgeType() const { return meEdgeType; } + EdgeDirection getEdgeDirection() const { return meEdgeDirection; } + + /// For STL sort + bool operator<( const SweepLineEvent& rRHS ) const { return mfPos < rRHS.mfPos; } + + private: + /// position of the event, in the direction of the line sweep + double mfPos; + + /** Rectangle this event is generated for + + This on the one hand saves some storage space (the + vector of rectangles is persistent, anyway), and on + the other hand provides an identifier to match active + edges and events (see below) + + Ptr because class needs to be assignable + */ + const B2DRectangle* mpAssociatedRect; + + /// 'upper' or 'lower' edge of original rectangle. + EdgeType meEdgeType; + + /// 'up' or 'down' + EdgeDirection meEdgeDirection; + }; + + typedef std::vector< SweepLineEvent > VectorOfEvents; + + /** Smart point container for B2DMultiRange::getPolyPolygon() + + This class provides methods needed only here, and is used + as a place to store some additional information per + polygon. Also, most of the intersection logic is + implemented here. + */ + class ImplPolygon + { + public: + /** Create polygon + */ + ImplPolygon() : + mpLeadingRightEdge(nullptr), + mnIdx(-1), + mbIsFinished(false) + { + // completely ad-hoc. but what the hell. + maPoints.reserve(11); + } + + void setPolygonPoolIndex( std::ptrdiff_t nIdx ) { mnIdx = nIdx; } + + /// Add point to the end of the existing points + void append( const B2DPoint& rPoint ) + { + OSL_PRECOND( maPoints.empty() || + maPoints.back().getX() == rPoint.getX() || + maPoints.back().getY() == rPoint.getY(), + "ImplPolygon::append(): added point violates 90 degree line angle constraint!" ); + + if( maPoints.empty() || + maPoints.back() != rPoint ) + { + // avoid duplicate points + maPoints.push_back( rPoint ); + } + } + + /** Perform the intersection of this polygon with an + active edge. + + @param rEvent + The vertical line event that generated the + intersection + + @param rActiveEdge + The active edge that generated the intersection + + @param rPolygonPool + Polygon pool, we sometimes need to allocate a new one + + @param bIsFinishingEdge + True, when this is hitting the last edge of the + vertical sweep - every vertical sweep starts and ends + with upper and lower edge of the _same_ rectangle. + + @return the new current polygon (that's the one + processing must proceed with, when going through the + list of upcoming active edges). + */ + std::ptrdiff_t intersect( SweepLineEvent const & rEvent, + ActiveEdge& rActiveEdge, + VectorOfPolygons& rPolygonPool, + B2DPolyPolygon& rRes, + bool isFinishingEdge ) + { + OSL_PRECOND( !mbIsFinished, + "ImplPolygon::intersect(): called on already finished polygon!" ); + OSL_PRECOND( !isFinishingEdge || &rEvent.getRect() == &rActiveEdge.getRect(), + "ImplPolygon::intersect(): inconsistent ending!" ); + + const B2DPoint aIntersectionPoint( rEvent.getPos(), + rActiveEdge.getInvariantCoord() ); + + // intersection point, goes to our polygon + // unconditionally + append(aIntersectionPoint); + + if( isFinishingEdge ) + { + // isSweepLineEnteringRect ? + if( rEvent.getEdgeType() == SweepLineEvent::STARTING_EDGE) + handleFinalOwnRightEdge(rActiveEdge); + else + handleFinalOwnLeftEdge(rActiveEdge, + rPolygonPool, + rRes); + + // we're done with this rect & sweep line + return -1; + } + else if( metOwnEdge(rEvent,rActiveEdge) ) + { + handleInitialOwnEdge(rEvent, rActiveEdge); + + // point already added, all init done, continue + // with same poly + return mnIdx; + } + else + { + OSL_ENSURE( rActiveEdge.getTargetPolygonIndex() != -1, + "ImplPolygon::intersect(): non-trivial intersection hit empty polygon!" ); + + const bool isHittingLeftEdge( + rActiveEdge.getEdgeDirection() == ActiveEdge::PROCEED_LEFT); + + if( isHittingLeftEdge ) + return handleComplexLeftEdge(rActiveEdge, + aIntersectionPoint, + rPolygonPool, + rRes); + else + return handleComplexRightEdge(rActiveEdge, + aIntersectionPoint, + rPolygonPool); + } + } + + private: + void handleInitialOwnEdge(SweepLineEvent const & rEvent, + ActiveEdge const & rActiveEdge) const + { + const bool isActiveEdgeProceedLeft( + rActiveEdge.getEdgeDirection() == ActiveEdge::PROCEED_LEFT); + const bool isSweepLineEnteringRect( + rEvent.getEdgeType() == SweepLineEvent::STARTING_EDGE); + + OSL_ENSURE( isSweepLineEnteringRect == isActiveEdgeProceedLeft, + "ImplPolygon::intersect(): sweep initial own edge hit: wrong polygon order" ); + + OSL_ENSURE( isSweepLineEnteringRect || + mpLeadingRightEdge == &rActiveEdge, + "ImplPolygon::intersect(): sweep initial own edge hit: wrong leading edge" ); + } + + void handleFinalOwnRightEdge(ActiveEdge& rActiveEdge) + { + OSL_ENSURE( rActiveEdge.getEdgeDirection() == ActiveEdge::PROCEED_RIGHT, + "ImplPolygon::handleInitialOwnRightEdge(): start edge wrong polygon order" ); + + rActiveEdge.setTargetPolygonIndex(mnIdx); + mpLeadingRightEdge = &rActiveEdge; + } + + void handleFinalOwnLeftEdge(ActiveEdge const & rActiveEdge, + VectorOfPolygons& rPolygonPool, + B2DPolyPolygon& rRes) + { + OSL_ENSURE( rActiveEdge.getEdgeDirection() == ActiveEdge::PROCEED_LEFT, + "ImplPolygon::handleFinalOwnLeftEdge(): end edge wrong polygon order" ); + + const bool isHittingOurTail( + rActiveEdge.getTargetPolygonIndex() == mnIdx); + + if( isHittingOurTail ) + finish(rRes); // just finish. no fuss. + else + { + // temp poly hits final left edge + const std::ptrdiff_t nTmpIdx=rActiveEdge.getTargetPolygonIndex(); + ImplPolygon& rTmp=rPolygonPool.get(nTmpIdx); + + // active edge's polygon has points + // already. ours need to go in front of them. + maPoints.insert(maPoints.end(), + rTmp.maPoints.begin(), + rTmp.maPoints.end()); + + // adjust leading edges, we're switching the polygon + ActiveEdge* const pFarEdge=rTmp.mpLeadingRightEdge; + + mpLeadingRightEdge = pFarEdge; + pFarEdge->setTargetPolygonIndex(mnIdx); + + // nTmpIdx is an empty shell, get rid of it + rPolygonPool.free(nTmpIdx); + } + } + + std::ptrdiff_t handleComplexLeftEdge(ActiveEdge& rActiveEdge, + const B2DPoint& rIntersectionPoint, + VectorOfPolygons& rPolygonPool, + B2DPolyPolygon& rRes) + { + const bool isHittingOurTail( + rActiveEdge.getTargetPolygonIndex() == mnIdx); + if( isHittingOurTail ) + { + finish(rRes); + + // so "this" is done - need new polygon to collect + // further points + const std::ptrdiff_t nIdxNewPolygon=rPolygonPool.alloc(); + rPolygonPool.get(nIdxNewPolygon).setPolygonPoolIndex(nIdxNewPolygon); + rPolygonPool.get(nIdxNewPolygon).append(rIntersectionPoint); + + rActiveEdge.setTargetPolygonIndex(nIdxNewPolygon); + + return nIdxNewPolygon; + } + else + { + const std::ptrdiff_t nTmpIdx=rActiveEdge.getTargetPolygonIndex(); + ImplPolygon& rTmp=rPolygonPool.get(nTmpIdx); + + // active edge's polygon has points + // already. ours need to go in front of them. + maPoints.insert(maPoints.end(), + rTmp.maPoints.begin(), + rTmp.maPoints.end()); + + rTmp.maPoints.clear(); + rTmp.append(rIntersectionPoint); + + // adjust leading edges, we're switching the polygon + ActiveEdge* const pFarEdge=rTmp.mpLeadingRightEdge; + ActiveEdge* const pNearEdge=&rActiveEdge; + + rTmp.mpLeadingRightEdge = nullptr; + pNearEdge->setTargetPolygonIndex(nTmpIdx); + + mpLeadingRightEdge = pFarEdge; + pFarEdge->setTargetPolygonIndex(mnIdx); + + return nTmpIdx; + } + } + + std::ptrdiff_t handleComplexRightEdge(ActiveEdge& rActiveEdge, + const B2DPoint& rIntersectionPoint, + VectorOfPolygons& rPolygonPool) + { + const std::ptrdiff_t nTmpIdx=rActiveEdge.getTargetPolygonIndex(); + ImplPolygon& rTmp=rPolygonPool.get(nTmpIdx); + + rTmp.append(rIntersectionPoint); + + rActiveEdge.setTargetPolygonIndex(mnIdx); + mpLeadingRightEdge = &rActiveEdge; + + rTmp.mpLeadingRightEdge = nullptr; + + return nTmpIdx; + } + + /// True when sweep line hits our own active edge + static bool metOwnEdge(SweepLineEvent const & rEvent, + ActiveEdge const & rActiveEdge) + { + const bool bHitOwnEdge=&rEvent.getRect() == &rActiveEdge.getRect(); + return bHitOwnEdge; + } + + /// Retrieve B2DPolygon from this object + B2DPolygon getPolygon() const + { + B2DPolygon aRes; + for (auto const& aPoint : maPoints) + aRes.append(aPoint, 1); + aRes.setClosed( true ); + return aRes; + } + + /** Finish this polygon, push to result set. + */ + void finish(B2DPolyPolygon& rRes) + { + OSL_PRECOND( maPoints.empty() || + maPoints.front().getX() == maPoints.back().getX() || + maPoints.front().getY() == maPoints.back().getY(), + "ImplPolygon::finish(): first and last point violate 90 degree line angle constraint!" ); + + mbIsFinished = true; + mpLeadingRightEdge = nullptr; + + rRes.append(getPolygon()); + } + + /** Refers to the current leading edge element of this + polygon, or NULL. The leading edge denotes the 'front' + of the polygon vertex sequence, i.e. the coordinates + at the polygon's leading edge are returned from + maPoints.front() + */ + ActiveEdge* mpLeadingRightEdge; + + /// current index into vector pool + std::ptrdiff_t mnIdx; + + /// Container for the actual polygon points + std::vector<B2DPoint> maPoints; + + /// When true, this polygon is 'done', i.e. nothing must be added anymore. + bool mbIsFinished; + }; + + /** Init sweep line event list + + This method fills the event list with the sweep line + events generated from the input rectangles, and sorts them + with increasing x. + */ + void setupSweepLineEventListFromRanges( VectorOfEvents& o_rEventVector, + const std::vector<B2DRange>& rRanges, + const std::vector<B2VectorOrientation>& rOrientations ) + { + // we need exactly 2*rectVec.size() events: one for the + // left, and one for the right edge of each rectangle + o_rEventVector.clear(); + o_rEventVector.reserve( 2*rRanges.size() ); + + // generate events + // =============== + + // first pass: add all left edges in increasing order + std::vector<B2DRange>::const_iterator aCurrRect=rRanges.begin(); + std::vector<B2VectorOrientation>::const_iterator aCurrOrientation=rOrientations.begin(); + const std::vector<B2DRange>::const_iterator aEnd=rRanges.end(); + const std::vector<B2VectorOrientation>::const_iterator aEndOrientation=rOrientations.end(); + while( aCurrRect != aEnd && aCurrOrientation != aEndOrientation ) + { + const B2DRectangle& rCurrRect( *aCurrRect++ ); + + o_rEventVector.emplace_back( rCurrRect.getMinX(), + rCurrRect, + SweepLineEvent::STARTING_EDGE, + (*aCurrOrientation++) == B2VectorOrientation::Positive ? + SweepLineEvent::PROCEED_UP : SweepLineEvent::PROCEED_DOWN ); + } + + // second pass: add all right edges in reversed order + std::vector<B2DRange>::const_reverse_iterator aCurrRectR=rRanges.rbegin(); + std::vector<B2VectorOrientation>::const_reverse_iterator aCurrOrientationR=rOrientations.rbegin(); + const std::vector<B2DRange>::const_reverse_iterator aEndR=rRanges.rend(); + while( aCurrRectR != aEndR ) + { + const B2DRectangle& rCurrRect( *aCurrRectR++ ); + + o_rEventVector.emplace_back( rCurrRect.getMaxX(), + rCurrRect, + SweepLineEvent::FINISHING_EDGE, + (*aCurrOrientationR++) == B2VectorOrientation::Positive ? + SweepLineEvent::PROCEED_DOWN : SweepLineEvent::PROCEED_UP ); + } + + // sort events + // =========== + + // since we use stable_sort, the order of events with the + // same x value will not change. The elaborate two-pass + // add above thus ensures, that for each two rectangles + // with similar left and right x coordinates, the + // rectangle whose left event comes first will have its + // right event come last. This is advantageous for the + // clip algorithm below, see handleRightEdgeCrossing(). + + std::stable_sort( o_rEventVector.begin(), + o_rEventVector.end() ); + } + + /** Insert two active edge segments for the given rectangle. + + This method creates two active edge segments from the + given rect, and inserts them into the active edge list, + such that this stays sorted (if it was before). + + @param io_rEdgeList + Active edge list to insert into + + @param io_rPolygons + Vector of polygons. Each rectangle added creates one + tentative result polygon in this vector, and the edge list + entries holds a reference to that polygon (this _requires_ + that the polygon vector does not reallocate, i.e. it must + have at least the maximal number of rectangles reserved) + + @param o_CurrentPolygon + The then-current polygon when processing this sweep line + event + + @param rCurrEvent + The actual event that caused this call + */ + void createActiveEdgesFromStartEvent( ListOfEdges & io_rEdgeList, + VectorOfPolygons & io_rPolygonPool, + SweepLineEvent const & rCurrEvent ) + { + ListOfEdges aNewEdges; + const B2DRectangle& rRect=rCurrEvent.getRect(); + const bool bGoesDown=rCurrEvent.getEdgeDirection() == SweepLineEvent::PROCEED_DOWN; + + // start event - new rect starts here, needs polygon to + // collect points into + const std::ptrdiff_t nIdxPolygon=io_rPolygonPool.alloc(); + io_rPolygonPool.get(nIdxPolygon).setPolygonPoolIndex(nIdxPolygon); + + // upper edge + aNewEdges.emplace_back( + rRect, + rRect.getMinY(), + bGoesDown ? nIdxPolygon : -1, + bGoesDown ? ActiveEdge::PROCEED_LEFT : ActiveEdge::PROCEED_RIGHT ); + // lower edge + aNewEdges.emplace_back( + rRect, + rRect.getMaxY(), + bGoesDown ? -1 : nIdxPolygon, + bGoesDown ? ActiveEdge::PROCEED_RIGHT : ActiveEdge::PROCEED_LEFT ); + + // furthermore, have to respect a special tie-breaking + // rule here, for edges which share the same y value: + // newly added upper edges must be inserted _before_ any + // other edge with the same y value, and newly added lower + // edges must be _after_ all other edges with the same + // y. This ensures that the left vertical edge processing + // below encounters the upper edge of the current rect + // first, and the lower edge last, which automatically + // starts and finishes this rect correctly (as only then, + // the polygon will have their associated active edges + // set). + const double nMinY( rRect.getMinY() ); + const double nMaxY( rRect.getMaxY() ); + ListOfEdges::iterator aCurr( io_rEdgeList.begin() ); + const ListOfEdges::iterator aEnd ( io_rEdgeList.end() ); + while( aCurr != aEnd ) + { + const double nCurrY( aCurr->getInvariantCoord() ); + + if( nCurrY >= nMinY && + aNewEdges.size() == 2 ) // only add, if not yet done. + { + // insert upper edge _before_ aCurr. Thus, it will + // be the first entry for a range of equal y + // values. Using splice here, since we hold + // references to the moved list element! + io_rEdgeList.splice( aCurr, + aNewEdges, + aNewEdges.begin() ); + } + + if( nCurrY > nMaxY ) + { + // insert lower edge _before_ aCurr. Thus, it will + // be the last entry for a range of equal y values + // (aCurr is the first entry strictly larger than + // nMaxY). Using splice here, since we hold + // references to the moved list element! + io_rEdgeList.splice( aCurr, + aNewEdges, + aNewEdges.begin() ); + // done with insertion, can early-exit here. + return; + } + + ++aCurr; + } + + // append remainder of aNewList (might still contain 2 or + // 1 elements, depending of the contents of io_rEdgeList). + io_rEdgeList.splice( aCurr, + aNewEdges ); + } + + bool isSameRect(ActiveEdge const & rEdge, + basegfx::B2DRange const & rRect) + { + return &rEdge.getRect() == &rRect; + } + + // wow what a hack. necessary because stl's list::erase does + // not eat reverse_iterator + template<typename Cont, typename Iter> Iter eraseFromList(Cont&, const Iter&); + template<> ListOfEdges::iterator eraseFromList( + ListOfEdges& rList, const ListOfEdges::iterator& aIter) + { + return rList.erase(aIter); + } + template<> ListOfEdges::reverse_iterator eraseFromList( + ListOfEdges& rList, const ListOfEdges::reverse_iterator& aIter) + { + return ListOfEdges::reverse_iterator( + rList.erase(std::prev(aIter.base()))); + } + + template<int bPerformErase, + typename Iterator> void processActiveEdges( + Iterator first, + Iterator last, + ListOfEdges& rActiveEdgeList, + SweepLineEvent const & rCurrEvent, + VectorOfPolygons& rPolygonPool, + B2DPolyPolygon& rRes ) + { + const basegfx::B2DRange& rCurrRect=rCurrEvent.getRect(); + + // fast-forward to rCurrEvent's first active edge (holds + // for both starting and finishing sweep line events, a + // rect is regarded _outside_ any rects whose events have + // started earlier + first = std::find_if(first, last, + [&rCurrRect](ActiveEdge& anEdge) { return isSameRect(anEdge, rCurrRect); }); + + if(first == last) + return; + + int nCount=0; + std::ptrdiff_t nCurrPolyIdx=-1; + while(first != last) + { + if( nCurrPolyIdx == -1 ) + nCurrPolyIdx=first->getTargetPolygonIndex(); + + assert(nCurrPolyIdx != -1); + + // second encounter of my rect -> second edge + // encountered, done + const bool bExit= + nCount && + isSameRect(*first, + rCurrRect); + + // deal with current active edge + nCurrPolyIdx = + rPolygonPool.get(nCurrPolyIdx).intersect( + rCurrEvent, + *first, + rPolygonPool, + rRes, + bExit); + + // prune upper & lower active edges, if requested + if( bPerformErase && (bExit || !nCount) ) + first = eraseFromList(rActiveEdgeList,first); + else + ++first; + + // delayed exit, had to prune first + if( bExit ) + return; + + ++nCount; + } + } + + template<int bPerformErase> void processActiveEdgesTopDown( + SweepLineEvent& rCurrEvent, + ListOfEdges& rActiveEdgeList, + VectorOfPolygons& rPolygonPool, + B2DPolyPolygon& rRes ) + { + processActiveEdges<bPerformErase>( + rActiveEdgeList. begin(), + rActiveEdgeList. end(), + rActiveEdgeList, + rCurrEvent, + rPolygonPool, + rRes); + } + + template<int bPerformErase> void processActiveEdgesBottomUp( + SweepLineEvent& rCurrEvent, + ListOfEdges& rActiveEdgeList, + VectorOfPolygons& rPolygonPool, + B2DPolyPolygon& rRes ) + { + processActiveEdges<bPerformErase>( + rActiveEdgeList. rbegin(), + rActiveEdgeList. rend(), + rActiveEdgeList, + rCurrEvent, + rPolygonPool, + rRes); + } + + enum{ NoErase=0, PerformErase=1 }; + + void handleStartingEdge( SweepLineEvent& rCurrEvent, + ListOfEdges& rActiveEdgeList, + VectorOfPolygons& rPolygonPool, + B2DPolyPolygon& rRes) + { + // inject two new active edges for rect + createActiveEdgesFromStartEvent( rActiveEdgeList, + rPolygonPool, + rCurrEvent ); + + if( rCurrEvent.getEdgeDirection() == SweepLineEvent::PROCEED_DOWN ) + processActiveEdgesTopDown<NoErase>( + rCurrEvent, rActiveEdgeList, rPolygonPool, rRes); + else + processActiveEdgesBottomUp<NoErase>( + rCurrEvent, rActiveEdgeList, rPolygonPool, rRes); + } + + void handleFinishingEdge( SweepLineEvent& rCurrEvent, + ListOfEdges& rActiveEdgeList, + VectorOfPolygons& rPolygonPool, + B2DPolyPolygon& rRes) + { + if( rCurrEvent.getEdgeDirection() == SweepLineEvent::PROCEED_DOWN ) + processActiveEdgesTopDown<PerformErase>( + rCurrEvent, rActiveEdgeList, rPolygonPool, rRes); + else + processActiveEdgesBottomUp<PerformErase>( + rCurrEvent, rActiveEdgeList, rPolygonPool, rRes); + } + + void handleSweepLineEvent( SweepLineEvent& rCurrEvent, + ListOfEdges& rActiveEdgeList, + VectorOfPolygons& rPolygonPool, + B2DPolyPolygon& rRes) + { + if( rCurrEvent.getEdgeType() == SweepLineEvent::STARTING_EDGE ) + handleStartingEdge(rCurrEvent,rActiveEdgeList,rPolygonPool,rRes); + else + handleFinishingEdge(rCurrEvent,rActiveEdgeList,rPolygonPool,rRes); + } + } + + namespace utils + { + B2DPolyPolygon solveCrossovers(const std::vector<B2DRange>& rRanges, + const std::vector<B2VectorOrientation>& rOrientations) + { + // sweep-line algorithm to generate a poly-polygon + // from a bunch of rectangles + // =============================================== + + // This algorithm uses the well-known sweep line + // concept, explained in every good text book about + // computational geometry. + + // We start with creating two structures for every + // rectangle, one representing the left x coordinate, + // one representing the right x coordinate (and both + // referencing the original rect). These structs are + // sorted with increasing x coordinates. + + // Then, we start processing the resulting list from + // the beginning. Every entry in the list defines a + // point in time of the line sweeping from left to + // right across all rectangles. + VectorOfEvents aSweepLineEvents; + setupSweepLineEventListFromRanges( aSweepLineEvents, + rRanges, + rOrientations ); + + B2DPolyPolygon aRes; + VectorOfPolygons aPolygonPool; + ListOfEdges aActiveEdgeList; + + // sometimes not enough, but a usable compromise + aPolygonPool.reserve( rRanges.size() ); + + for (auto& aSweepLineEvent : aSweepLineEvents) + handleSweepLineEvent(aSweepLineEvent, aActiveEdgeList, aPolygonPool, aRes); + + return aRes; + } + } +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/basegfx/source/range/b2xrange.cxx b/basegfx/source/range/b2xrange.cxx new file mode 100644 index 0000000000..3f04a95af5 --- /dev/null +++ b/basegfx/source/range/b2xrange.cxx @@ -0,0 +1,120 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * 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 . + */ + +#include <basegfx/range/b2drange.hxx> +#include <basegfx/range/b2irange.hxx> + +namespace basegfx +{ + namespace + { + /** Generic implementation of the difference set computation + + @tpl RangeType + Type to operate on. Must provide ValueType and TraitsType + nested types. + */ + template< class RangeType > void doComputeSetDifference( + std::vector< RangeType >& o_rRanges, + const RangeType& a, + const RangeType& b ) + { + o_rRanges.clear(); + + // special-casing the empty rect case (this will fail most + // of the times below, because of the DBL_MIN/MAX special + // values denoting emptiness in the rectangle. + if( a.isEmpty() ) + { + o_rRanges.push_back( b ); + return; + } + if( b.isEmpty() ) + { + o_rRanges.push_back( a ); + return; + } + + const typename RangeType::ValueType ax(a.getMinX()); + const typename RangeType::ValueType ay(a.getMinY()); + const typename RangeType::TraitsType::DifferenceType aw(a.getWidth()); + const typename RangeType::TraitsType::DifferenceType ah(a.getHeight()); + const typename RangeType::ValueType bx(b.getMinX()); + const typename RangeType::ValueType by(b.getMinY()); + const typename RangeType::TraitsType::DifferenceType bw(b.getWidth()); + const typename RangeType::TraitsType::DifferenceType bh(b.getHeight()); + + const typename RangeType::TraitsType::DifferenceType h0( (by > ay) ? by - ay : 0 ); + const typename RangeType::TraitsType::DifferenceType h3( (by + bh < ay + ah) ? ay + ah - by - bh : 0 ); + const typename RangeType::TraitsType::DifferenceType w1( (bx > ax) ? bx - ax : 0 ); + const typename RangeType::TraitsType::DifferenceType w2( (ax + aw > bx + bw) ? ax + aw - bx - bw : 0 ); + const typename RangeType::TraitsType::DifferenceType h12( (h0 + h3 < ah) ? ah - h0 - h3 : 0 ); + + // TODO(E2): Use numeric_cast instead of static_cast here, + // need range checks! + if (h0 > 0) + o_rRanges.push_back( + RangeType(ax,ay, + static_cast<typename RangeType::ValueType>(ax+aw), + static_cast<typename RangeType::ValueType>(ay+h0)) ); + + if (w1 > 0 && h12 > 0) + o_rRanges.push_back( + RangeType(ax, + static_cast<typename RangeType::ValueType>(ay+h0), + static_cast<typename RangeType::ValueType>(ax+w1), + static_cast<typename RangeType::ValueType>(ay+h0+h12)) ); + + if (w2 > 0 && h12 > 0) + o_rRanges.push_back( + RangeType(static_cast<typename RangeType::ValueType>(bx+bw), + static_cast<typename RangeType::ValueType>(ay+h0), + static_cast<typename RangeType::ValueType>(bx+bw+w2), + static_cast<typename RangeType::ValueType>(ay+h0+h12)) ); + + if (h3 > 0) + o_rRanges.push_back( + RangeType(ax, + static_cast<typename RangeType::ValueType>(ay+h0+h12), + static_cast<typename RangeType::ValueType>(ax+aw), + static_cast<typename RangeType::ValueType>(ay+h0+h12+h3)) ); + } + } + + std::vector< B2IRange >& computeSetDifference( std::vector< B2IRange >& o_rResult, + const B2IRange& rFirst, + const B2IRange& rSecond ) + { + doComputeSetDifference( o_rResult, rFirst, rSecond ); + + return o_rResult; + } + + std::vector< B2DRange >& computeSetDifference( std::vector< B2DRange >& o_rResult, + const B2DRange& rFirst, + const B2DRange& rSecond ) + { + doComputeSetDifference( o_rResult, rFirst, rSecond ); + + return o_rResult; + } + +} // end of namespace basegfx + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/basegfx/source/range/b3drange.cxx b/basegfx/source/range/b3drange.cxx new file mode 100644 index 0000000000..89bb98a1e1 --- /dev/null +++ b/basegfx/source/range/b3drange.cxx @@ -0,0 +1,64 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * 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 . + */ + +#include <basegfx/range/b3drange.hxx> +#include <basegfx/matrix/b3dhommatrix.hxx> + +namespace basegfx +{ + void B3DRange::transform(const B3DHomMatrix& rMatrix) + { + if(isEmpty() || rMatrix.isIdentity()) + return; + + const B3DRange aSource(*this); + reset(); + expand(rMatrix * B3DPoint(aSource.getMinX(), aSource.getMinY(), aSource.getMinZ())); + expand(rMatrix * B3DPoint(aSource.getMaxX(), aSource.getMinY(), aSource.getMinZ())); + expand(rMatrix * B3DPoint(aSource.getMinX(), aSource.getMaxY(), aSource.getMinZ())); + expand(rMatrix * B3DPoint(aSource.getMaxX(), aSource.getMaxY(), aSource.getMinZ())); + expand(rMatrix * B3DPoint(aSource.getMinX(), aSource.getMinY(), aSource.getMaxZ())); + expand(rMatrix * B3DPoint(aSource.getMaxX(), aSource.getMinY(), aSource.getMaxZ())); + expand(rMatrix * B3DPoint(aSource.getMinX(), aSource.getMaxY(), aSource.getMaxZ())); + expand(rMatrix * B3DPoint(aSource.getMaxX(), aSource.getMaxY(), aSource.getMaxZ())); + } + + B3DRange& B3DRange::operator*=( const ::basegfx::B3DHomMatrix& rMat ) + { + transform(rMat); + return *this; + } + + const B3DRange& B3DRange::getUnitB3DRange() + { + static const B3DRange aUnitB3DRange(0.0, 0.0, 0.0, 1.0, 1.0, 1.0); + + return aUnitB3DRange; + } + + B3DRange operator*( const ::basegfx::B3DHomMatrix& rMat, const B3DRange& rB3DRange ) + { + B3DRange aRes( rB3DRange ); + aRes *= rMat; + return aRes; + } + +} // end of namespace basegfx + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/basegfx/source/raster/rasterconvert3d.cxx b/basegfx/source/raster/rasterconvert3d.cxx new file mode 100644 index 0000000000..e998eb9938 --- /dev/null +++ b/basegfx/source/raster/rasterconvert3d.cxx @@ -0,0 +1,337 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * 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 . + */ + +#include <basegfx/raster/rasterconvert3d.hxx> +#include <basegfx/vector/b2dvector.hxx> +#include <basegfx/polygon/b3dpolygon.hxx> +#include <basegfx/polygon/b3dpolypolygon.hxx> +#include <basegfx/point/b3dpoint.hxx> + +// implementations of the 3D raster converter + +namespace basegfx +{ + void RasterConverter3D::addArea(const B3DPolygon& rFill, const B3DHomMatrix* pViewToEye) + { + const sal_uInt32 nPointCount(rFill.count()); + + for(sal_uInt32 a(0); a < nPointCount; a++) + { + addEdge(rFill, a, (a + 1) % nPointCount, pViewToEye); + } + } + + void RasterConverter3D::addArea(const B3DPolyPolygon& rFill, const B3DHomMatrix* pViewToEye) + { + const sal_uInt32 nPolyCount(rFill.count()); + + for(sal_uInt32 a(0); a < nPolyCount; a++) + { + addArea(rFill.getB3DPolygon(a), pViewToEye); + } + } + + RasterConverter3D::RasterConverter3D() + {} + + RasterConverter3D::~RasterConverter3D() + {} + + void RasterConverter3D::rasterconvertB3DArea(sal_Int32 nStartLine, sal_Int32 nStopLine) + { + if(maLineEntries.empty()) + return; + + OSL_ENSURE(nStopLine >= nStartLine, "nStopLine is bigger than nStartLine (!)"); + + // sort global entries by Y, X once. After this, the vector + // is seen as frozen. Pointers to its entries will be used in the following code. + std::sort(maLineEntries.begin(), maLineEntries.end()); + + // local parameters + std::vector< RasterConversionLineEntry3D >::iterator aCurrentEntry(maLineEntries.begin()); + std::vector< RasterConversionLineEntry3D* > aCurrentLine; + std::vector< RasterConversionLineEntry3D* > aNextLine; + std::vector< RasterConversionLineEntry3D* >::iterator aRasterConversionLineEntry3D; + + // get scanlines first LineNumber as start + sal_Int32 nLineNumber(std::max(aCurrentEntry->getY(), nStartLine)); + + while((!aCurrentLine.empty() || aCurrentEntry != maLineEntries.end()) && (nLineNumber < nStopLine)) + { + // add all entries which start at current line to current scanline + while(aCurrentEntry != maLineEntries.end()) + { + const sal_Int32 nCurrentLineNumber(aCurrentEntry->getY()); + + if(nCurrentLineNumber > nLineNumber) + { + // line is below current one, done (since array is sorted) + break; + } + else + { + // less or equal. Line is above or at current one. Advance it exactly to + // current line + const sal_uInt32 nStep(nLineNumber - nCurrentLineNumber); + + if(!nStep || aCurrentEntry->decrementRasterConversionLineEntry3D(nStep)) + { + // add when exactly on current line or when increment to it did not + // completely consume it + if(nStep) + { + aCurrentEntry->incrementRasterConversionLineEntry3D(nStep, *this); + } + + aCurrentLine.push_back(&(*aCurrentEntry)); + } + } + + ++aCurrentEntry; + } + + // sort current scanline using comparator. Only X is used there + // since all entries are already in one processed line. This needs to be done + // every time since not only new spans may have benn added or old removed, + // but incrementing may also have changed the order + std::sort(aCurrentLine.begin(), aCurrentLine.end(), lineComparator()); + + // process current scanline + aRasterConversionLineEntry3D = aCurrentLine.begin(); + aNextLine.clear(); + sal_uInt32 nPairCount(0); + + while(aRasterConversionLineEntry3D != aCurrentLine.end()) + { + RasterConversionLineEntry3D& rPrevScanRasterConversionLineEntry3D(**aRasterConversionLineEntry3D++); + + // look for 2nd span + if(aRasterConversionLineEntry3D != aCurrentLine.end()) + { + // work on span from rPrevScanRasterConversionLineEntry3D to aRasterConversionLineEntry3D, fLineNumber is valid + processLineSpan(rPrevScanRasterConversionLineEntry3D, **aRasterConversionLineEntry3D, nLineNumber, nPairCount++); + } + + // increment to next line + if(rPrevScanRasterConversionLineEntry3D.decrementRasterConversionLineEntry3D(1)) + { + rPrevScanRasterConversionLineEntry3D.incrementRasterConversionLineEntry3D(1, *this); + aNextLine.push_back(&rPrevScanRasterConversionLineEntry3D); + } + } + + // copy back next scanline if count has changed + if(aNextLine.size() != aCurrentLine.size()) + { + aCurrentLine = aNextLine; + } + + // increment fLineNumber + nLineNumber++; + } + } + + void RasterConverter3D::addEdge(const B3DPolygon& rFill, sal_uInt32 a, sal_uInt32 b, const B3DHomMatrix* pViewToEye) + { + B3DPoint aStart(rFill.getB3DPoint(a)); + B3DPoint aEnd(rFill.getB3DPoint(b)); + sal_Int32 nYStart(fround(aStart.getY())); + sal_Int32 nYEnd(fround(aEnd.getY())); + + if(nYStart == nYEnd) + return; + + if(nYStart > nYEnd) + { + std::swap(aStart, aEnd); + std::swap(nYStart, nYEnd); + std::swap(a, b); + } + + const sal_uInt32 nYDelta(nYEnd - nYStart); + const double fInvYDelta(1.0 / nYDelta); + maLineEntries.emplace_back( + aStart.getX(), (aEnd.getX() - aStart.getX()) * fInvYDelta, + aStart.getZ(), (aEnd.getZ() - aStart.getZ()) * fInvYDelta, + nYStart, nYDelta); + + // if extra interpolation data is used, add it to the last created entry + RasterConversionLineEntry3D& rEntry = maLineEntries[maLineEntries.size() - 1]; + + if(rFill.areBColorsUsed()) + { + rEntry.setColorIndex(addColorInterpolator(rFill.getBColor(a), rFill.getBColor(b), fInvYDelta)); + } + + if(rFill.areNormalsUsed()) + { + rEntry.setNormalIndex(addNormalInterpolator(rFill.getNormal(a), rFill.getNormal(b), fInvYDelta)); + } + + if(!rFill.areTextureCoordinatesUsed()) + return; + + if(pViewToEye) + { + const double fEyeA(((*pViewToEye) * aStart).getZ()); + const double fEyeB(((*pViewToEye) * aEnd).getZ()); + + rEntry.setInverseTextureIndex(addInverseTextureInterpolator( + rFill.getTextureCoordinate(a), + rFill.getTextureCoordinate(b), + fEyeA, fEyeB, fInvYDelta)); + } + else + { + rEntry.setTextureIndex(addTextureInterpolator( + rFill.getTextureCoordinate(a), + rFill.getTextureCoordinate(b), + fInvYDelta)); + } + } + + void RasterConverter3D::rasterconvertB3DEdge(const B3DPolygon& rLine, sal_uInt32 nA, sal_uInt32 nB, sal_Int32 nStartLine, sal_Int32 nStopLine, sal_uInt16 nLineWidth) + { + B3DPoint aStart(rLine.getB3DPoint(nA)); + B3DPoint aEnd(rLine.getB3DPoint(nB)); + const double fZBufferLineAdd(0x00ff); + + if(nLineWidth > 1) + { + // this is not a hairline anymore, in most cases since it's an oversampled + // hairline to get e.g. AA for Z-Buffering. Create fill geometry. + if(!aStart.equal(aEnd)) + { + reset(); + maLineEntries.clear(); + + B2DVector aVector(aEnd.getX() - aStart.getX(), aEnd.getY() - aStart.getY()); + aVector.normalize(); + const B2DVector aPerpend(getPerpendicular(aVector) * ((static_cast<double>(nLineWidth) + 0.5) * 0.5)); + const double fZStartWithAdd(aStart.getZ() + fZBufferLineAdd); + const double fZEndWithAdd(aEnd.getZ() + fZBufferLineAdd); + + B3DPolygon aPolygon; + aPolygon.append(B3DPoint(aStart.getX() + aPerpend.getX(), aStart.getY() + aPerpend.getY(), fZStartWithAdd)); + aPolygon.append(B3DPoint(aEnd.getX() + aPerpend.getX(), aEnd.getY() + aPerpend.getY(), fZEndWithAdd)); + aPolygon.append(B3DPoint(aEnd.getX() - aPerpend.getX(), aEnd.getY() - aPerpend.getY(), fZEndWithAdd)); + aPolygon.append(B3DPoint(aStart.getX() - aPerpend.getX(), aStart.getY() - aPerpend.getY(), fZStartWithAdd)); + aPolygon.setClosed(true); + + addArea(aPolygon, nullptr); + } + } + else + { + // it's a hairline. Use direct RasterConversionLineEntry creation to + // rasterconvert lines as similar to areas as possible to avoid Z-Fighting + sal_Int32 nYStart(fround(aStart.getY())); + sal_Int32 nYEnd(fround(aEnd.getY())); + + if(nYStart == nYEnd) + { + // horizontal line, check X + const sal_Int32 nXStart(static_cast<sal_Int32>(aStart.getX())); + const sal_Int32 nXEnd(static_cast<sal_Int32>(aEnd.getX())); + + if(nXStart != nXEnd) + { + reset(); + maLineEntries.clear(); + + // horizontal line, create vertical entries. These will be sorted by + // X anyways, so no need to distinguish the case here + maLineEntries.emplace_back( + aStart.getX(), 0.0, + aStart.getZ() + fZBufferLineAdd, 0.0, + nYStart, 1); + maLineEntries.emplace_back( + aEnd.getX(), 0.0, + aEnd.getZ() + fZBufferLineAdd, 0.0, + nYStart, 1); + } + } + else + { + reset(); + maLineEntries.clear(); + + if(nYStart > nYEnd) + { + std::swap(aStart, aEnd); + std::swap(nYStart, nYEnd); + } + + const sal_uInt32 nYDelta(static_cast<sal_uInt32>(nYEnd - nYStart)); + const double fInvYDelta(1.0 / nYDelta); + + // non-horizontal line, create two parallel entries. These will be sorted by + // X anyways, so no need to distinguish the case here + maLineEntries.emplace_back( + aStart.getX(), (aEnd.getX() - aStart.getX()) * fInvYDelta, + aStart.getZ() + fZBufferLineAdd, (aEnd.getZ() - aStart.getZ()) * fInvYDelta, + nYStart, nYDelta); + + RasterConversionLineEntry3D& rEntry = maLineEntries[maLineEntries.size() - 1]; + + // need to choose a X-Distance for the 2nd edge which guarantees all pixels + // of the line to be set. This is exactly the X-Increment for one Y-Step. + // Same is true for Z, so in both cases, add one increment to them. To also + // guarantee one pixel per line, add a minimum of one for X. + const double fDistanceX(fabs(rEntry.getX().getInc()) >= 1.0 ? rEntry.getX().getInc() : 1.0); + + maLineEntries.emplace_back( + rEntry.getX().getVal() + fDistanceX, rEntry.getX().getInc(), + rEntry.getZ().getVal() + rEntry.getZ().getInc(), rEntry.getZ().getInc(), + nYStart, nYDelta); + } + } + + if(!maLineEntries.empty()) + { + rasterconvertB3DArea(nStartLine, nStopLine); + } + } + + void RasterConverter3D::rasterconvertB3DPolyPolygon(const B3DPolyPolygon& rFill, const B3DHomMatrix* pViewToEye, sal_Int32 nStartLine, sal_Int32 nStopLine) + { + reset(); + maLineEntries.clear(); + addArea(rFill, pViewToEye); + rasterconvertB3DArea(nStartLine, nStopLine); + } + + void RasterConverter3D::rasterconvertB3DPolygon(const B3DPolygon& rLine, sal_Int32 nStartLine, sal_Int32 nStopLine, sal_uInt16 nLineWidth) + { + const sal_uInt32 nPointCount(rLine.count()); + + if(nPointCount) + { + const sal_uInt32 nEdgeCount(rLine.isClosed() ? nPointCount : nPointCount - 1); + + for(sal_uInt32 a(0); a < nEdgeCount; a++) + { + rasterconvertB3DEdge(rLine, a, (a + 1) % nPointCount, nStartLine, nStopLine, nLineWidth); + } + } + } +} // end of namespace basegfx + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/basegfx/source/tools/b2dclipstate.cxx b/basegfx/source/tools/b2dclipstate.cxx new file mode 100644 index 0000000000..126235699b --- /dev/null +++ b/basegfx/source/tools/b2dclipstate.cxx @@ -0,0 +1,476 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * 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 . + */ + +#include <basegfx/utils/b2dclipstate.hxx> + +#include <basegfx/range/b2drange.hxx> +#include <basegfx/range/b2dpolyrange.hxx> +#include <basegfx/polygon/b2dpolygon.hxx> +#include <basegfx/polygon/b2dpolygontools.hxx> +#include <basegfx/polygon/b2dpolypolygon.hxx> +#include <basegfx/polygon/b2dpolypolygontools.hxx> +#include <basegfx/polygon/b2dpolypolygoncutter.hxx> +#include <utility> + + +namespace basegfx::utils +{ + class ImplB2DClipState + { + public: + enum Operation {UNION, INTERSECT, XOR, SUBTRACT}; + + ImplB2DClipState() : + mePendingOps(UNION) + {} + + explicit ImplB2DClipState( B2DPolyPolygon aPoly ) : + maClipPoly(std::move(aPoly)), + mePendingOps(UNION) + {} + + bool isCleared() const + { + return !maClipPoly.count() + && !maPendingPolygons.count() + && !maPendingRanges.count(); + } + + bool isNullClipPoly() const + { + return maClipPoly.count() == 1 + && !maClipPoly.getB2DPolygon(0).count(); + } + + bool isNull() const + { + return !maPendingPolygons.count() + && !maPendingRanges.count() + && isNullClipPoly(); + } + + void makeNull() + { + maPendingPolygons.clear(); + maPendingRanges.clear(); + maClipPoly.clear(); + maClipPoly.append(B2DPolygon()); + mePendingOps = UNION; + } + + bool operator==(const ImplB2DClipState& rRHS) const + { + return maPendingPolygons == rRHS.maPendingPolygons + && maPendingRanges == rRHS.maPendingRanges + && maClipPoly == rRHS.maClipPoly + && mePendingOps == rRHS.mePendingOps; + } + + void addRange(const B2DRange& rRange, Operation eOp) + { + if( rRange.isEmpty() ) + return; + + commitPendingPolygons(); + if( mePendingOps != eOp ) + commitPendingRanges(); + + mePendingOps = eOp; + maPendingRanges.appendElement( + rRange, + B2VectorOrientation::Positive); + } + + void addPolyPolygon(const B2DPolyPolygon& aPoly, Operation eOp) + { + commitPendingRanges(); + if( mePendingOps != eOp ) + commitPendingPolygons(); + + mePendingOps = eOp; + maPendingPolygons.append(aPoly); + } + + void unionRange(const B2DRange& rRange) + { + if( isCleared() ) + return; + + addRange(rRange,UNION); + } + + void unionPolyPolygon(const B2DPolyPolygon& rPolyPoly) + { + if( isCleared() ) + return; + + addPolyPolygon(rPolyPoly,UNION); + } + + void intersectRange(const B2DRange& rRange) + { + if( isNull() ) + return; + + addRange(rRange,INTERSECT); + } + + void intersectPolyPolygon(const B2DPolyPolygon& rPolyPoly) + { + if( isNull() ) + return; + + addPolyPolygon(rPolyPoly,INTERSECT); + } + + void subtractRange(const B2DRange& rRange ) + { + if( isNull() ) + return; + + addRange(rRange,SUBTRACT); + } + + void subtractPolyPolygon(const B2DPolyPolygon& rPolyPoly) + { + if( isNull() ) + return; + + addPolyPolygon(rPolyPoly,SUBTRACT); + } + + void xorRange(const B2DRange& rRange) + { + addRange(rRange,XOR); + } + + void xorPolyPolygon(const B2DPolyPolygon& rPolyPoly) + { + addPolyPolygon(rPolyPoly,XOR); + } + + void transform(const basegfx::B2DHomMatrix& rTranslate) + { + maPendingRanges.transform(rTranslate); + maPendingPolygons.transform(rTranslate); + maClipPoly.transform(rTranslate); + } + + B2DPolyPolygon const & getClipPoly() const + { + commitPendingRanges(); + commitPendingPolygons(); + + return maClipPoly; + } + + private: + void commitPendingPolygons() const + { + if( !maPendingPolygons.count() ) + return; + + // assumption: maClipPoly has kept polygons prepared for + // clipping; i.e. no neutral polygons & correct + // orientation + maPendingPolygons = utils::prepareForPolygonOperation(maPendingPolygons); + const bool bIsEmpty=isNullClipPoly(); + const bool bIsCleared=!maClipPoly.count(); + switch(mePendingOps) + { + case UNION: + assert( !bIsCleared ); + + if( bIsEmpty ) + maClipPoly = maPendingPolygons; + else + maClipPoly = utils::solvePolygonOperationOr( + maClipPoly, + maPendingPolygons); + break; + case INTERSECT: + assert( !bIsEmpty ); + + if( bIsCleared ) + maClipPoly = maPendingPolygons; + else + maClipPoly = utils::solvePolygonOperationAnd( + maClipPoly, + maPendingPolygons); + break; + case XOR: + if( bIsEmpty ) + maClipPoly = maPendingPolygons; + else if( bIsCleared ) + { + // not representable, strictly speaking, + // using polygons with the common even/odd + // or nonzero winding number fill rule. If + // we'd want to represent it, fill rule + // would need to be "non-negative winding + // number" (and we then would return + // 'holes' here) + + // going for an ugly hack meanwhile + maClipPoly = utils::solvePolygonOperationXor( + B2DPolyPolygon( + utils::createPolygonFromRect(B2DRange(-1E20,-1E20,1E20,1E20))), + maPendingPolygons); + } + else + maClipPoly = utils::solvePolygonOperationXor( + maClipPoly, + maPendingPolygons); + break; + case SUBTRACT: + assert( !bIsEmpty ); + + // first union all pending ones, subtract en bloc then + maPendingPolygons = solveCrossovers(maPendingPolygons); + maPendingPolygons = stripNeutralPolygons(maPendingPolygons); + maPendingPolygons = stripDispensablePolygons(maPendingPolygons); + + if( bIsCleared ) + { + // not representable, strictly speaking, + // using polygons with the common even/odd + // or nonzero winding number fill rule. If + // we'd want to represent it, fill rule + // would need to be "non-negative winding + // number" (and we then would return + // 'holes' here) + + // going for an ugly hack meanwhile + maClipPoly = utils::solvePolygonOperationDiff( + B2DPolyPolygon( + utils::createPolygonFromRect(B2DRange(-1E20,-1E20,1E20,1E20))), + maPendingPolygons); + } + else + maClipPoly = utils::solvePolygonOperationDiff( + maClipPoly, + maPendingPolygons); + break; + } + + maPendingPolygons.clear(); + mePendingOps = UNION; + } + + void commitPendingRanges() const + { + if( !maPendingRanges.count() ) + return; + + // use the specialized range clipper for the win + B2DPolyPolygon aCollectedRanges; + const bool bIsEmpty=isNullClipPoly(); + const bool bIsCleared=!maClipPoly.count(); + switch(mePendingOps) + { + case UNION: + assert( !bIsCleared ); + + aCollectedRanges = maPendingRanges.solveCrossovers(); + aCollectedRanges = stripNeutralPolygons(aCollectedRanges); + aCollectedRanges = stripDispensablePolygons(aCollectedRanges); + if( bIsEmpty ) + maClipPoly = aCollectedRanges; + else + maClipPoly = utils::solvePolygonOperationOr( + maClipPoly, + aCollectedRanges); + break; + case INTERSECT: + assert( !bIsEmpty ); + + aCollectedRanges = maPendingRanges.solveCrossovers(); + aCollectedRanges = stripNeutralPolygons(aCollectedRanges); + if( maPendingRanges.count() > 1 ) + aCollectedRanges = stripDispensablePolygons(aCollectedRanges, true); + + if( bIsCleared ) + maClipPoly = aCollectedRanges; + else + maClipPoly = utils::solvePolygonOperationAnd( + maClipPoly, + aCollectedRanges); + break; + case XOR: + aCollectedRanges = maPendingRanges.solveCrossovers(); + aCollectedRanges = stripNeutralPolygons(aCollectedRanges); + aCollectedRanges = correctOrientations(aCollectedRanges); + + if( bIsEmpty ) + maClipPoly = aCollectedRanges; + else if( bIsCleared ) + { + // not representable, strictly speaking, + // using polygons with the common even/odd + // or nonzero winding number fill rule. If + // we'd want to represent it, fill rule + // would need to be "non-negative winding + // number" (and we then would return + // 'holes' here) + + // going for an ugly hack meanwhile + maClipPoly = utils::solvePolygonOperationXor( + B2DPolyPolygon( + utils::createPolygonFromRect(B2DRange(-1E20,-1E20,1E20,1E20))), + aCollectedRanges); + } + else + maClipPoly = utils::solvePolygonOperationXor( + maClipPoly, + aCollectedRanges); + break; + case SUBTRACT: + assert( !bIsEmpty ); + + // first union all pending ranges, subtract en bloc then + aCollectedRanges = maPendingRanges.solveCrossovers(); + aCollectedRanges = stripNeutralPolygons(aCollectedRanges); + aCollectedRanges = stripDispensablePolygons(aCollectedRanges); + + if( bIsCleared ) + { + // not representable, strictly speaking, + // using polygons with the common even/odd + // or nonzero winding number fill rule. If + // we'd want to represent it, fill rule + // would need to be "non-negative winding + // number" (and we then would return + // 'holes' here) + + // going for an ugly hack meanwhile + maClipPoly = utils::solvePolygonOperationDiff( + B2DPolyPolygon( + utils::createPolygonFromRect(B2DRange(-1E20,-1E20,1E20,1E20))), + aCollectedRanges); + } + else + maClipPoly = utils::solvePolygonOperationDiff( + maClipPoly, + aCollectedRanges); + break; + } + + maPendingRanges.clear(); + mePendingOps = UNION; + } + + mutable B2DPolyPolygon maPendingPolygons; + mutable B2DPolyRange maPendingRanges; + mutable B2DPolyPolygon maClipPoly; + mutable Operation mePendingOps; + }; + + B2DClipState::B2DClipState() = default; + + B2DClipState::~B2DClipState() = default; + + B2DClipState::B2DClipState( const B2DClipState& ) = default; + + B2DClipState::B2DClipState( B2DClipState&& ) = default; + + B2DClipState::B2DClipState( const B2DPolyPolygon& rPolyPoly ) : + mpImpl( ImplB2DClipState(rPolyPoly) ) + {} + + B2DClipState& B2DClipState::operator=( const B2DClipState& ) = default; + + B2DClipState& B2DClipState::operator=( B2DClipState&& ) = default; + + void B2DClipState::makeNull() + { + mpImpl->makeNull(); + } + + bool B2DClipState::isCleared() const + { + return mpImpl->isCleared(); + } + + bool B2DClipState::operator==(const B2DClipState& rRHS) const + { + if(mpImpl.same_object(rRHS.mpImpl)) + return true; + + return ((*mpImpl) == (*rRHS.mpImpl)); + } + + bool B2DClipState::operator!=(const B2DClipState& rRHS) const + { + return !(*this == rRHS); + } + + void B2DClipState::unionRange(const B2DRange& rRange) + { + mpImpl->unionRange(rRange); + } + + void B2DClipState::unionPolyPolygon(const B2DPolyPolygon& rPolyPoly) + { + mpImpl->unionPolyPolygon(rPolyPoly); + } + + void B2DClipState::intersectRange(const B2DRange& rRange) + { + mpImpl->intersectRange(rRange); + } + + void B2DClipState::intersectPolyPolygon(const B2DPolyPolygon& rPolyPoly) + { + mpImpl->intersectPolyPolygon(rPolyPoly); + } + + void B2DClipState::subtractRange(const B2DRange& rRange) + { + mpImpl->subtractRange(rRange); + } + + void B2DClipState::subtractPolyPolygon(const B2DPolyPolygon& rPolyPoly) + { + mpImpl->subtractPolyPolygon(rPolyPoly); + } + + void B2DClipState::xorRange(const B2DRange& rRange) + { + mpImpl->xorRange(rRange); + } + + void B2DClipState::xorPolyPolygon(const B2DPolyPolygon& rPolyPoly) + { + mpImpl->xorPolyPolygon(rPolyPoly); + } + + B2DPolyPolygon const & B2DClipState::getClipPoly() const + { + return mpImpl->getClipPoly(); + } + + void B2DClipState::transform(const basegfx::B2DHomMatrix& rTranslate) + { + return mpImpl->transform(rTranslate); + } + + +} // end of namespace + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/basegfx/source/tools/bgradient.cxx b/basegfx/source/tools/bgradient.cxx new file mode 100644 index 0000000000..86e1812d21 --- /dev/null +++ b/basegfx/source/tools/bgradient.cxx @@ -0,0 +1,997 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include <basegfx/utils/bgradient.hxx> +#include <basegfx/utils/gradienttools.hxx> +#include <com/sun/star/awt/Gradient2.hpp> +#include <boost/property_tree/json_parser.hpp> +#include <map> + +typedef std::map<OUString, OUString> StringMap; + +namespace +{ +css::awt::GradientStyle lcl_getStyleFromString(std::u16string_view rStyle) +{ + if (rStyle == u"LINEAR") + return css::awt::GradientStyle_LINEAR; + else if (rStyle == u"AXIAL") + return css::awt::GradientStyle_AXIAL; + else if (rStyle == u"RADIAL") + return css::awt::GradientStyle_RADIAL; + else if (rStyle == u"ELLIPTICAL") + return css::awt::GradientStyle_ELLIPTICAL; + else if (rStyle == u"SQUARE") + return css::awt::GradientStyle_SQUARE; + else if (rStyle == u"RECT") + return css::awt::GradientStyle_RECT; + + return css::awt::GradientStyle_LINEAR; +} + +StringMap lcl_jsonToStringMap(std::u16string_view rJSON) +{ + StringMap aArgs; + if (rJSON.size() && rJSON[0] != '\0') + { + std::stringstream aStream(std::string(OUStringToOString(rJSON, RTL_TEXTENCODING_ASCII_US))); + boost::property_tree::ptree aTree; + boost::property_tree::read_json(aStream, aTree); + + for (const auto& rPair : aTree) + { + aArgs[OUString::fromUtf8(rPair.first)] + = OUString::fromUtf8(rPair.second.get_value<std::string>(".")); + } + } + return aArgs; +} + +basegfx::BGradient lcl_buildGradientFromStringMap(StringMap& rMap) +{ + basegfx::BGradient aGradient( + basegfx::BColorStops(ColorToBColorConverter(rMap["startcolor"].toInt32(16)).getBColor(), + ColorToBColorConverter(rMap["endcolor"].toInt32(16)).getBColor())); + + aGradient.SetGradientStyle(lcl_getStyleFromString(rMap["style"])); + aGradient.SetAngle(Degree10(rMap["angle"].toInt32())); + + return aGradient; +} +} + +namespace basegfx +{ +// constructor with two colors to explicitly create a +// BColorStops for a single StartColor @0.0 & EndColor @1.0 +BColorStops::BColorStops(const BColor& rStart, const BColor& rEnd) +{ + emplace_back(0.0, rStart); + emplace_back(1.0, rEnd); +} + +/* Helper to grep the correct ColorStop out of + ColorStops and interpolate as needed for given + relative value in fPosition in the range of [0.0 .. 1.0]. + It also takes care of evtl. given RequestedSteps. + */ +BColor BColorStops::getInterpolatedBColor(double fPosition, sal_uInt32 nRequestedSteps, + BColorStopRange& rLastColorStopRange) const +{ + // no color at all, done + if (empty()) + return BColor(); + + // outside range -> at start + const double fMin(front().getStopOffset()); + if (fPosition < fMin) + return front().getStopColor(); + + // outside range -> at end + const double fMax(back().getStopOffset()); + if (fPosition > fMax) + return back().getStopColor(); + + // special case for the 'classic' case with just two colors: + // we can optimize that and keep the speed/resources low + // by avoiding some calculations and an O(log(N)) array access + if (2 == size()) + { + // if same StopOffset use front color + if (fTools::equal(fMin, fMax)) + return front().getStopColor(); + + const basegfx::BColor aCStart(front().getStopColor()); + const basegfx::BColor aCEnd(back().getStopColor()); + + // if colors are equal just return one + if (aCStart == aCEnd) + return aCStart; + + // calculate Steps + const sal_uInt32 nSteps( + basegfx::utils::calculateNumberOfSteps(nRequestedSteps, aCStart, aCEnd)); + + // we need to extend the interpolation to the local + // range of ColorStops. Despite having two ColorStops + // these are not necessarily at 0.0 and 1.0, so may be + // not the classical Start/EndColor (what is allowed) + fPosition = (fPosition - fMin) / (fMax - fMin); + return basegfx::interpolate(aCStart, aCEnd, + nSteps > 1 ? floor(fPosition * nSteps) / double(nSteps - 1) + : fPosition); + } + + // check if we need to newly populate the needed interpolation data + // or if we can re-use from last time. + // If this scope is not entered, we do not need the binary search. It's + // only a single buffered entry, and only used when more than three + // ColorStops exist, but makes a huge difference compared with accessing + // the sorted ColorStop vector each time. + // NOTE: with this simple change I get very high hit rates, e.g. rotating + // a donut with gradient test '1' hit rate is at 0.99909440357755486 + if (rLastColorStopRange.mfOffsetStart == rLastColorStopRange.mfOffsetEnd + || fPosition < rLastColorStopRange.mfOffsetStart + || fPosition > rLastColorStopRange.mfOffsetEnd) + { + // access needed spot in sorted array using binary search + // NOTE: This *seems* slow(er) when developing compared to just + // looping/accessing, but that's just due to the extensive + // debug test code created by the stl. In a pro version, + // all is good/fast as expected + const auto upperBound(std::upper_bound(begin(), end(), BColorStop(fPosition), + [](const BColorStop& x, const BColorStop& y) { + return x.getStopOffset() < y.getStopOffset(); + })); + + // no upper bound, done + if (end() == upperBound) + return back().getStopColor(); + + // lower bound is one entry back, access that + const auto lowerBound(upperBound - 1); + + // no lower bound, done + if (end() == lowerBound) + return back().getStopColor(); + + // we have lower and upper bound, get colors and offsets + rLastColorStopRange.maColorStart = lowerBound->getStopColor(); + rLastColorStopRange.maColorEnd = upperBound->getStopColor(); + rLastColorStopRange.mfOffsetStart = lowerBound->getStopOffset(); + rLastColorStopRange.mfOffsetEnd = upperBound->getStopOffset(); + } + + // when there are just two color steps this cannot happen, but when using + // a range of colors this *may* be used inside the range to represent + // single-colored regions inside a ColorRange. Use that color & done + if (rLastColorStopRange.maColorStart == rLastColorStopRange.maColorEnd) + return rLastColorStopRange.maColorStart; + + // calculate number of steps and adapted proportional + // range for scaler in [0.0 .. 1.0] + const double fAdaptedScaler( + (fPosition - rLastColorStopRange.mfOffsetStart) + / (rLastColorStopRange.mfOffsetEnd - rLastColorStopRange.mfOffsetStart)); + const sal_uInt32 nSteps(basegfx::utils::calculateNumberOfSteps( + nRequestedSteps, rLastColorStopRange.maColorStart, rLastColorStopRange.maColorEnd)); + + // interpolate & evtl. apply steps + return interpolate(rLastColorStopRange.maColorStart, rLastColorStopRange.maColorEnd, + nSteps > 1 ? floor(fAdaptedScaler * nSteps) / double(nSteps - 1) + : fAdaptedScaler); +} + +/* Tooling method that allows to replace the StartColor in a + vector of ColorStops. A vector in 'ordered state' is expected, + so you may use/have used sortAndCorrect. + This method is for convenience & backwards compatibility, please + think about handling multi-colored gradients directly. + */ +void BColorStops::replaceStartColor(const BColor& rStart) +{ + BColorStops::iterator a1stNonStartColor(begin()); + + // search for highest existing non-StartColor - CAUTION, + // there might be none, one or multiple with StopOffset 0.0 + while (a1stNonStartColor != end() + && basegfx::fTools::lessOrEqual(a1stNonStartColor->getStopOffset(), 0.0)) + a1stNonStartColor++; + + // create new ColorStops by 1st adding new one and then all + // non-StartColor entries + BColorStops aNewColorStops; + + aNewColorStops.reserve(size() + 1); + aNewColorStops.emplace_back(0.0, rStart); + aNewColorStops.insert(aNewColorStops.end(), a1stNonStartColor, end()); + + // assign & done + *this = aNewColorStops; +} + +/* Tooling method that allows to replace the EndColor in a + vector of ColorStops. A vector in 'ordered state' is expected, + so you may use/have used sortAndCorrectColorStops. + This method is for convenience & backwards compatibility, please + think about handling multi-colored gradients directly. + */ +void BColorStops::replaceEndColor(const BColor& rEnd) +{ + // erase all evtl. existing EndColor(s) + while (!empty() && basegfx::fTools::moreOrEqual(back().getStopOffset(), 1.0)) + pop_back(); + + // add at the end of existing ColorStops + emplace_back(1.0, rEnd); +} + +/* Tooling method to linearly blend the Colors contained in + a given ColorStop vector against a given Color using the + given intensity values. + The intensity values fStartIntensity, fEndIntensity are + in the range of [0.0 .. 1.0] and describe how much the + blend is supposed to be done at the start color position + and the end color position respectively, where 0.0 means + to fully use the given BlendColor, 1.0 means to not change + the existing color in the ColorStop. + Every color entry in the given ColorStop is blended + relative to it's StopPosition, interpolating the + given intensities with the range [0.0 .. 1.0] to do so. + */ +void BColorStops::blendToIntensity(double fStartIntensity, double fEndIntensity, + const BColor& rBlendColor) +{ + // no entries, done + if (empty()) + return; + + // correct intensities (maybe assert when input was wrong) + fStartIntensity = std::max(std::min(1.0, fStartIntensity), 0.0); + fEndIntensity = std::max(std::min(1.0, fEndIntensity), 0.0); + + // all 100%, no real blend, done + if (basegfx::fTools::equal(fStartIntensity, 1.0) && basegfx::fTools::equal(fEndIntensity, 1.0)) + return; + + // blend relative to StopOffset position + for (auto& candidate : *this) + { + const double fOffset(candidate.getStopOffset()); + const double fIntensity((fStartIntensity * (1.0 - fOffset)) + (fEndIntensity * fOffset)); + candidate = basegfx::BColorStop( + fOffset, basegfx::interpolate(rBlendColor, candidate.getStopColor(), fIntensity)); + } +} + +/* Tooling method to guarantee sort and correctness for + the given ColorStops vector. + A vector fulfilling these conditions is called to be + in 'ordered state'. + + At return, the following conditions are guaranteed: + - contains no ColorStops with offset < 0.0 (will + be removed) + - contains no ColorStops with offset > 1.0 (will + be removed) + - ColorStops with identical offsets are now allowed + - will be sorted from lowest offset to highest + + Some more notes: + - It can happen that the result is empty + - It is allowed to have consecutive entries with + the same color, this represents single-color + regions inside the gradient + - A entry with 0.0 is not required or forced, so + no 'StartColor' is technically required + - A entry with 1.0 is not required or forced, so + no 'EndColor' is technically required + + All this is done in one run (sort + O(N)) without + creating a copy of the data in any form + */ +void BColorStops::sortAndCorrect() +{ + // no content, we are done + if (empty()) + return; + + if (1 == size()) + { + // no gradient at all, but preserve given color + // evtl. correct offset to be in valid range [0.0 .. 1.0] + // NOTE: This does not move it to 0.0 or 1.0, it *can* still + // be somewhere in-between what is allowed + const BColorStop aEntry(front()); + clear(); + emplace_back(std::max(0.0, std::min(1.0, aEntry.getStopOffset())), aEntry.getStopColor()); + + // done + return; + } + + // start with sorting the input data. Remember that + // this preserves the order of equal entries, where + // equal is defined here by offset (see use operator==) + std::sort(begin(), end()); + + // prepare status values + size_t write(0); + + // use the paradigm of a band machine with two heads, read + // and write with write <= read all the time. Step over the + // data using read and check for valid entry. If valid, decide + // how to keep it + for (size_t read(0); read < size(); read++) + { + // get offset of entry at read position + double fOff((*this)[read].getStopOffset()); + + if (basegfx::fTools::less(fOff, 0.0) && read + 1 < size()) + { + // value < 0.0 and we have a next entry. check for gradient snippet + // containing 0.0 resp. StartColor + const double fOff2((*this)[read + 1].getStopOffset()); + + if (basegfx::fTools::more(fOff2, 0.0)) + { + // read is the start of a gradient snippet containing 0.0. Correct + // entry to StartColor, interpolate to correct StartColor + (*this)[read] + = BColorStop(0.0, basegfx::interpolate((*this)[read].getStopColor(), + (*this)[read + 1].getStopColor(), + (0.0 - fOff) / (fOff2 - fOff))); + + // adapt fOff + fOff = 0.0; + } + } + + // step over < 0 values, these are outside and will be removed + if (basegfx::fTools::less(fOff, 0.0)) + { + continue; + } + + if (basegfx::fTools::less(fOff, 1.0) && read + 1 < size()) + { + // value < 1.0 and we have a next entry. check for gradient snippet + // containing 1.0 resp. EndColor + const double fOff2((*this)[read + 1].getStopOffset()); + + if (basegfx::fTools::more(fOff2, 1.0)) + { + // read is the start of a gradient snippet containing 1.0. Correct + // next entry to EndColor, interpolate to correct EndColor + (*this)[read + 1] + = BColorStop(1.0, basegfx::interpolate((*this)[read].getStopColor(), + (*this)[read + 1].getStopColor(), + (1.0 - fOff) / (fOff2 - fOff))); + + // adapt fOff + fOff = 1.0; + } + } + + // step over > 1 values; even break, since all following + // entries will also be bigger due to being sorted, so done + if (basegfx::fTools::more(fOff, 1.0)) + { + break; + } + + // entry is valid value at read position + // copy if write target is empty (write at start) or when + // write target is different to read in color or offset + if (0 == write || !((*this)[read] == (*this)[write - 1])) + { + if (write != read) + { + // copy read to write backwards to close gaps + (*this)[write] = (*this)[read]; + } + + // always forward write position + write++; + } + } + + // correct size when length is reduced. write is always at + // last used position + 1 + if (size() > write) + { + if (0 == write) + { + // no valid entries at all, but not empty. This can only happen + // when all entries are below 0.0 or above 1.0 (else a gradient + // snippet spawning over both would have been detected) + if (basegfx::fTools::less(back().getStopOffset(), 0.0)) + { + // all outside too low, rescue last due to being closest to content + const BColor aBackColor(back().getStopColor()); + clear(); + emplace_back(0.0, aBackColor); + } + else // if (basegfx::fTools::more(front().getStopOffset(), 1.0)) + { + // all outside too high, rescue first due to being closest to content + const BColor aFrontColor(front().getStopColor()); + clear(); + emplace_back(1.0, aFrontColor); + } + } + else + { + resize(write); + } + } +} + +bool BColorStops::checkPenultimate() const +{ + // not needed when no ColorStops + if (empty()) + return false; + + // not needed when last ColorStop at the end or outside + if (basegfx::fTools::moreOrEqual(back().getStopOffset(), 1.0)) + return false; + + // get penultimate entry + const auto penultimate(rbegin() + 1); + + // if there is none, we need no correction and are done + if (penultimate == rend()) + return false; + + // not needed when the last two ColorStops have different offset, then + // a visible range will be processed already + if (!basegfx::fTools::equal(back().getStopOffset(), penultimate->getStopOffset())) + return false; + + // not needed when the last two ColorStops have the same Color, then the + // range before solves the problem + if (back().getStopColor() == penultimate->getStopColor()) + return false; + + return true; +} + +/* Tooling method to check if a ColorStop vector is defined + by a single color. It returns true if this is the case. + If true is returned, rSingleColor contains that single + color for convenience. + NOTE: If no ColorStop is defined, a fallback to BColor-default + (which is black) and true will be returned + */ +bool BColorStops::isSingleColor(BColor& rSingleColor) const +{ + if (empty()) + { + rSingleColor = BColor(); + return true; + } + + if (1 == size()) + { + rSingleColor = front().getStopColor(); + return true; + } + + rSingleColor = front().getStopColor(); + + for (auto const& rCandidate : *this) + { + if (rCandidate.getStopColor() != rSingleColor) + return false; + } + + return true; +} + +/* Tooling method to reverse ColorStops, including offsets. + When also mirroring offsets a valid sort keeps valid. + */ +void BColorStops::reverseColorStops() +{ + // can use std::reverse, but also need to adapt offset(s) + std::reverse(begin(), end()); + for (auto& candidate : *this) + candidate = BColorStop(1.0 - candidate.getStopOffset(), candidate.getStopColor()); +} + +// createSpaceAtStart creates fOffset space at start by +// translating/scaling all entries to the right +void BColorStops::createSpaceAtStart(double fOffset) +{ + // nothing to do if empty + if (empty()) + return; + + // correct offset to [0.0 .. 1.0] + fOffset = std::max(std::min(1.0, fOffset), 0.0); + + // nothing to do if 0.0 == offset + if (basegfx::fTools::equalZero(fOffset)) + return; + + BColorStops aNewStops; + + for (const auto& candidate : *this) + { + aNewStops.emplace_back(fOffset + (candidate.getStopOffset() * (1.0 - fOffset)), + candidate.getStopColor()); + } + + *this = aNewStops; +} + +// removeSpaceAtStart removes fOffset space from start by +// translating/scaling entries more or equal to fOffset +// to the left. Entries less than fOffset will be removed +void BColorStops::removeSpaceAtStart(double fOffset) +{ + // nothing to do if empty + if (empty()) + return; + + // correct factor to [0.0 .. 1.0] + fOffset = std::max(std::min(1.0, fOffset), 0.0); + + // nothing to do if fOffset == 0.0 + if (basegfx::fTools::equalZero(fOffset)) + return; + + BColorStops aNewStops; + const double fMul(basegfx::fTools::equal(fOffset, 1.0) ? 1.0 : 1.0 / (1.0 - fOffset)); + + for (const auto& candidate : *this) + { + if (basegfx::fTools::moreOrEqual(candidate.getStopOffset(), fOffset)) + { + aNewStops.emplace_back((candidate.getStopOffset() - fOffset) * fMul, + candidate.getStopColor()); + } + } + + *this = aNewStops; +} + +// try to detect if an empty/no-color-change area exists +// at the start and return offset to it. Returns 0.0 if not. +double BColorStops::detectPossibleOffsetAtStart() const +{ + BColor aSingleColor; + const bool bSingleColor(isSingleColor(aSingleColor)); + + // no useful offset for single color + if (bSingleColor) + return 0.0; + + // here we know that we have at least two colors, so we have a + // color change. Find colors left and right of that first color change + BColorStops::const_iterator aColorR(begin()); + BColorStops::const_iterator aColorL(aColorR++); + + // aColorR would 1st get equal to end(), so no need to also check aColorL + // for end(). Loop as long as same color. Since we *have* a color change + // not even aColorR can get equal to end() before color inequality, but + // keep for safety + while (aColorR != end() && aColorL->getStopColor() == aColorR->getStopColor()) + { + aColorL++; + aColorR++; + } + + // also for safety: access values at aColorL below *only* + // if not equal to end(), but can theoretically not happen + if (aColorL == end()) + { + return 0.0; + } + + // return offset (maybe 0.0 what is OK) + return aColorL->getStopOffset(); +} + +// checks whether the color stops are symmetrical in color and offset. +bool BColorStops::isSymmetrical() const +{ + if (empty()) + return false; + if (1 == size()) + return basegfx::fTools::equal(0.5, front().getStopOffset()); + + BColorStops::const_iterator aIter(begin()); // for going forward + BColorStops::const_iterator aRIter(end()); // for going backward + --aRIter; + // We have at least two elements, so aIter <= aRIter fails before iterators no longer point to + // an element. + while (aIter <= aRIter && aIter->getStopColor().equal(aRIter->getStopColor()) + && basegfx::fTools::equal(aIter->getStopOffset(), 1.0 - aRIter->getStopOffset())) + { + ++aIter; + --aRIter; + } + return aIter > aRIter; +} + +void BColorStops::doApplyAxial() +{ + // prepare new ColorStops + basegfx::BColorStops aNewColorStops; + + // add gradient stops in reverse order, scaled to [0.0 .. 0.5] + basegfx::BColorStops::const_reverse_iterator aRevCurrColor(rbegin()); + + while (aRevCurrColor != rend()) + { + aNewColorStops.emplace_back((1.0 - aRevCurrColor->getStopOffset()) * 0.5, + aRevCurrColor->getStopColor()); + aRevCurrColor++; + } + + // prepare non-reverse run + basegfx::BColorStops::const_iterator aCurrColor(begin()); + + if (basegfx::fTools::equalZero(aCurrColor->getStopOffset())) + { + // Caution: do not add 1st entry again, that would be double since it was + // already added as last element of the inverse run above. But only if + // the gradient has a start entry for 0.0 aka StartColor, else it is correct. + aCurrColor++; + } + + // add gradient stops in non-reverse order, translated and scaled to [0.5 .. 1.0] + while (aCurrColor != end()) + { + aNewColorStops.emplace_back((aCurrColor->getStopOffset() * 0.5) + 0.5, + aCurrColor->getStopColor()); + aCurrColor++; + } + + // apply color stops + *this = aNewColorStops; +} + +void BColorStops::doApplySteps(sal_uInt16 nStepCount) +{ + // check for zero or invalid steps setting -> done + if (0 == nStepCount || nStepCount > 100) + return; + + // no change needed if single color + BColor aSingleColor; + if (isSingleColor(aSingleColor)) + return; + + // prepare new color stops, get L/R iterators for segments + basegfx::BColorStops aNewColorStops; + basegfx::BColorStops::const_iterator aColorR(begin()); + basegfx::BColorStops::const_iterator aColorL(aColorR++); + + while (aColorR != end()) + { + // get start/end color for segment + const double fStart(aColorL->getStopOffset()); + const double fDelta(aColorR->getStopOffset() - fStart); + + if (aNewColorStops.empty() || aNewColorStops.back() != *aColorL) + { + // add start color, but check if it is already there - which is the + // case from the 2nd segment on due to a new segment starting with + // the same color as the previous one ended + aNewColorStops.push_back(*aColorL); + } + if (!basegfx::fTools::equalZero(fDelta)) + { + // create in-between steps, always two at the same position to + // define a 'hard' color stop. Get start/end color for the segment + const basegfx::BColor& rStartColor(aColorL->getStopColor()); + const basegfx::BColor& rEndColor(aColorR->getStopColor()); + + if (rStartColor != rEndColor) + { + // get relative single-step width + // tdf155852 Use same method for the color as in rendering. + const double fSingleStep(1.0 / static_cast<double>(nStepCount - 1)); + const double fOffsetStep(fDelta / static_cast<double>(nStepCount)); + + for (sal_uInt16 a(1); a < nStepCount; a++) + { + // calculate stop position since being used twice + const double fPosition(fStart + fOffsetStep * static_cast<double>(a)); + + // add end color of previous sub-segment + aNewColorStops.emplace_back( + fPosition, basegfx::interpolate(rStartColor, rEndColor, + static_cast<double>(a - 1) * fSingleStep)); + + // add start color of current sub-segment + aNewColorStops.emplace_back( + fPosition, basegfx::interpolate(rStartColor, rEndColor, + static_cast<double>(a) * fSingleStep)); + } + } + } + + // always add end color of segment + aNewColorStops.push_back(*aColorR); + + // next segment + aColorL++; + aColorR++; + } + + // apply the change to color stops + *this = aNewColorStops; +} + +std::string BGradient::GradientStyleToString(css::awt::GradientStyle eStyle) +{ + switch (eStyle) + { + case css::awt::GradientStyle::GradientStyle_LINEAR: + return "LINEAR"; + + case css::awt::GradientStyle::GradientStyle_AXIAL: + return "AXIAL"; + + case css::awt::GradientStyle::GradientStyle_RADIAL: + return "RADIAL"; + + case css::awt::GradientStyle::GradientStyle_ELLIPTICAL: + return "ELLIPTICAL"; + + case css::awt::GradientStyle::GradientStyle_SQUARE: + return "SQUARE"; + + case css::awt::GradientStyle::GradientStyle_RECT: + return "RECT"; + + case css::awt::GradientStyle::GradientStyle_MAKE_FIXED_SIZE: + return "MAKE_FIXED_SIZE"; + } + + return ""; +} + +BGradient BGradient::fromJSON(std::u16string_view rJSON) +{ + StringMap aMap(lcl_jsonToStringMap(rJSON)); + return lcl_buildGradientFromStringMap(aMap); +} + +BGradient::BGradient() + : eStyle(css::awt::GradientStyle_LINEAR) + , aColorStops() + , nAngle(0) + , nBorder(0) + , nOfsX(50) + , nOfsY(50) + , nIntensStart(100) + , nIntensEnd(100) + , nStepCount(0) +{ + aColorStops.emplace_back(0.0, BColor(0.0, 0.0, 0.0)); // COL_BLACK + aColorStops.emplace_back(1.0, BColor(1.0, 1.0, 1.0)); // COL_WHITE +} + +BGradient::BGradient(const basegfx::BColorStops& rColorStops, css::awt::GradientStyle eTheStyle, + Degree10 nTheAngle, sal_uInt16 nXOfs, sal_uInt16 nYOfs, sal_uInt16 nTheBorder, + sal_uInt16 nStartIntens, sal_uInt16 nEndIntens, sal_uInt16 nSteps) + : eStyle(eTheStyle) + , aColorStops(rColorStops) + , nAngle(nTheAngle) + , nBorder(nTheBorder) + , nOfsX(nXOfs) + , nOfsY(nYOfs) + , nIntensStart(nStartIntens) + , nIntensEnd(nEndIntens) + , nStepCount(nSteps) +{ + SetColorStops(aColorStops); +} + +bool BGradient::operator==(const BGradient& rGradient) const +{ + return (eStyle == rGradient.eStyle && aColorStops == rGradient.aColorStops + && nAngle == rGradient.nAngle && nBorder == rGradient.nBorder + && nOfsX == rGradient.nOfsX && nOfsY == rGradient.nOfsY + && nIntensStart == rGradient.nIntensStart && nIntensEnd == rGradient.nIntensEnd + && nStepCount == rGradient.nStepCount); +} + +void BGradient::SetColorStops(const basegfx::BColorStops& rSteps) +{ + aColorStops = rSteps; + aColorStops.sortAndCorrect(); + if (aColorStops.empty()) + aColorStops.emplace_back(0.0, basegfx::BColor()); +} + +namespace +{ +OUString AsRGBHexString(const ColorToBColorConverter& rVal) +{ + std::stringstream ss; + ss << std::hex << std::setfill('0') << std::setw(6) << sal_uInt32(rVal); + return OUString::createFromAscii(ss.str()); +} +} + +boost::property_tree::ptree BGradient::dumpAsJSON() const +{ + boost::property_tree::ptree aTree; + + aTree.put("style", BGradient::GradientStyleToString(eStyle)); + const ColorToBColorConverter aStart(GetColorStops().front().getStopColor()); + aTree.put("startcolor", AsRGBHexString(aStart.GetRGBColor())); + const ColorToBColorConverter aEnd(GetColorStops().back().getStopColor()); + aTree.put("endcolor", AsRGBHexString(aEnd.GetRGBColor())); + aTree.put("angle", std::to_string(nAngle.get())); + aTree.put("border", std::to_string(nBorder)); + aTree.put("x", std::to_string(nOfsX)); + aTree.put("y", std::to_string(nOfsY)); + aTree.put("intensstart", std::to_string(nIntensStart)); + aTree.put("intensend", std::to_string(nIntensEnd)); + aTree.put("stepcount", std::to_string(nStepCount)); + + return aTree; +} + +void BGradient::tryToRecreateBorder(basegfx::BColorStops* pAssociatedTransparencyStops) +{ + // border already set, do not try to recreate + if (0 != GetBorder()) + return; + + BColor aSingleColor; + const bool bSingleColor(GetColorStops().isSingleColor(aSingleColor)); + + // no need to recreate with single color + if (bSingleColor) + return; + + const bool bIsAxial(css::awt::GradientStyle_AXIAL == GetGradientStyle()); + + if (bIsAxial) + { + // for axial due to reverse used gradient work reversed + aColorStops.reverseColorStops(); + if (nullptr != pAssociatedTransparencyStops) + pAssociatedTransparencyStops->reverseColorStops(); + } + + // check if we have space at start of range [0.0 .. 1.0] that + // may be interpreted as 'border' -> same color. That may involve + // different scenarios, e.g. 1st index > 0.0, but also a non-zero + // number of same color entries, or a combination of both + const double fOffset(aColorStops.detectPossibleOffsetAtStart()); + + if (!basegfx::fTools::equalZero(fOffset)) + { + // we have a border area, indeed re-create + aColorStops.removeSpaceAtStart(fOffset); + if (nullptr != pAssociatedTransparencyStops) + pAssociatedTransparencyStops->removeSpaceAtStart(fOffset); + + // ...and create border value + SetBorder(static_cast<sal_uInt16>(std::lround(fOffset * 100.0))); + } + + if (bIsAxial) + { + // take back reverse + aColorStops.reverseColorStops(); + if (nullptr != pAssociatedTransparencyStops) + pAssociatedTransparencyStops->reverseColorStops(); + } +} + +void BGradient::tryToApplyBorder() +{ + // no border to apply, done + if (0 == GetBorder()) + return; + + // NOTE: no new start node is added. The new ColorStop + // mechanism does not need entries at 0.0 and 1.0. + // In case this is needed, do that in the caller + const double fOffset(GetBorder() * 0.01); + + if (css::awt::GradientStyle_AXIAL == GetGradientStyle()) + { + // for axial due to reverse used gradient work reversed + aColorStops.reverseColorStops(); + aColorStops.createSpaceAtStart(fOffset); + aColorStops.reverseColorStops(); + } + else + { + // apply border to GradientSteps + aColorStops.createSpaceAtStart(fOffset); + } + + // set changed values + SetBorder(0); +} + +void BGradient::tryToApplyStartEndIntensity() +{ + // already on default, nothing to apply + if (100 == GetStartIntens() && 100 == GetEndIntens()) + return; + + // apply 'old' blend stuff, blend against black + aColorStops.blendToIntensity(GetStartIntens() * 0.01, GetEndIntens() * 0.01, + BColor()); // COL_BLACK + + // set values to default + SetStartIntens(100); + SetEndIntens(100); +} + +void BGradient::tryToConvertToAxial() +{ + if (css::awt::GradientStyle_LINEAR != GetGradientStyle() || 0 != GetBorder() + || GetColorStops().empty()) + return; + + if (!GetColorStops().isSymmetrical()) + return; + + SetGradientStyle(css::awt::GradientStyle_AXIAL); + + // Stretch the first half of the color stops to double width + // and collect them in a new color stops vector. + BColorStops aAxialColorStops; + aAxialColorStops.reserve(std::ceil(GetColorStops().size() / 2.0)); + BColorStops::const_iterator aIter(GetColorStops().begin()); + while (basegfx::fTools::lessOrEqual(aIter->getStopOffset(), 0.5)) + { + BColorStop aNextStop(std::clamp((*aIter).getStopOffset() * 2.0, 0.0, 1.0), + (*aIter).getStopColor()); + aAxialColorStops.push_back(aNextStop); + ++aIter; + } + // Axial gradients have outmost color as last color stop. + aAxialColorStops.reverseColorStops(); + + SetColorStops(aAxialColorStops); +} + +void BGradient::tryToApplyAxial() +{ + // only need to do something if css::awt::GradientStyle_AXIAL, else done + if (GetGradientStyle() != css::awt::GradientStyle_AXIAL) + return; + + // apply the change to color stops + aColorStops.doApplyAxial(); + + // set style to GradientStyle_LINEAR + SetGradientStyle(css::awt::GradientStyle_LINEAR); +} + +void BGradient::tryToApplySteps() +{ + // check for zero or invalid steps setting -> done + if (0 == GetSteps() || GetSteps() > 100) + return; + + // do the action + aColorStops.doApplySteps(GetSteps()); + + // set value to default + SetSteps(0); +} +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/basegfx/source/tools/canvastools.cxx b/basegfx/source/tools/canvastools.cxx new file mode 100644 index 0000000000..70f4787710 --- /dev/null +++ b/basegfx/source/tools/canvastools.cxx @@ -0,0 +1,474 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * 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 . + */ + +#include <com/sun/star/geometry/RealSize2D.hpp> +#include <com/sun/star/geometry/RealPoint2D.hpp> +#include <com/sun/star/geometry/RealRectangle2D.hpp> +#include <com/sun/star/geometry/RealRectangle3D.hpp> +#include <com/sun/star/geometry/RealBezierSegment2D.hpp> +#include <com/sun/star/geometry/AffineMatrix2D.hpp> +#include <com/sun/star/geometry/AffineMatrix3D.hpp> +#include <com/sun/star/geometry/IntegerSize2D.hpp> +#include <com/sun/star/geometry/IntegerRectangle2D.hpp> +#include <com/sun/star/lang/IllegalArgumentException.hpp> +#include <com/sun/star/rendering/XPolyPolygon2D.hpp> +#include <com/sun/star/rendering/XGraphicDevice.hpp> +#include <com/sun/star/awt/Rectangle.hpp> +#include <basegfx/utils/unopolypolygon.hxx> +#include <basegfx/matrix/b2dhommatrix.hxx> +#include <basegfx/matrix/b3dhommatrix.hxx> +#include <basegfx/point/b2dpoint.hxx> +#include <basegfx/vector/b2dsize.hxx> +#include <basegfx/vector/b2ivector.hxx> +#include <basegfx/range/b3drange.hxx> +#include <basegfx/range/b2irange.hxx> +#include <basegfx/polygon/b2dpolygon.hxx> +#include <basegfx/polygon/b2dpolypolygon.hxx> +#include <basegfx/utils/canvastools.hxx> + +using namespace ::com::sun::star; + +namespace basegfx::unotools +{ + namespace + { + uno::Sequence< geometry::RealBezierSegment2D > bezierSequenceFromB2DPolygon(const ::basegfx::B2DPolygon& rPoly) + { + const sal_uInt32 nPointCount(rPoly.count()); + uno::Sequence< geometry::RealBezierSegment2D > outputSequence(nPointCount); + geometry::RealBezierSegment2D* pOutput = outputSequence.getArray(); + + // fill sequences and imply closed polygon on this implementation layer + for(sal_uInt32 a(0); a < nPointCount; a++) + { + const basegfx::B2DPoint aStart(rPoly.getB2DPoint(a)); + const basegfx::B2DPoint aControlA(rPoly.getNextControlPoint(a)); + const basegfx::B2DPoint aControlB(rPoly.getPrevControlPoint((a + 1) % nPointCount)); + + pOutput[a] = geometry::RealBezierSegment2D( + aStart.getX(), aStart.getY(), + aControlA.getX(), aControlA.getY(), + aControlB.getX(), aControlB.getY()); + } + + return outputSequence; + } + + uno::Sequence< geometry::RealPoint2D > pointSequenceFromB2DPolygon( const ::basegfx::B2DPolygon& rPoly ) + { + const sal_uInt32 nNumPoints( rPoly.count() ); + + uno::Sequence< geometry::RealPoint2D > outputSequence( nNumPoints ); + geometry::RealPoint2D* pOutput = outputSequence.getArray(); + + // fill sequence from polygon + sal_uInt32 i; + for( i=0; i<nNumPoints; ++i ) + { + const ::basegfx::B2DPoint aPoint( rPoly.getB2DPoint(i) ); + + pOutput[i] = geometry::RealPoint2D( aPoint.getX(), + aPoint.getY() ); + } + + return outputSequence; + } + } + + uno::Sequence< uno::Sequence< geometry::RealBezierSegment2D > > bezierSequenceSequenceFromB2DPolyPolygon( const ::basegfx::B2DPolyPolygon& rPolyPoly ) + { + const sal_uInt32 nNumPolies( rPolyPoly.count() ); + sal_uInt32 i; + + uno::Sequence< uno::Sequence< geometry::RealBezierSegment2D > > outputSequence( nNumPolies ); + uno::Sequence< geometry::RealBezierSegment2D >* pOutput = outputSequence.getArray(); + + for( i=0; i<nNumPolies; ++i ) + { + pOutput[i] = bezierSequenceFromB2DPolygon( rPolyPoly.getB2DPolygon(i) ); + } + + return outputSequence; + } + + uno::Sequence< uno::Sequence< geometry::RealPoint2D > > pointSequenceSequenceFromB2DPolyPolygon( const ::basegfx::B2DPolyPolygon& rPolyPoly ) + { + const sal_uInt32 nNumPolies( rPolyPoly.count() ); + sal_uInt32 i; + + uno::Sequence< uno::Sequence< geometry::RealPoint2D > > outputSequence( nNumPolies ); + uno::Sequence< geometry::RealPoint2D >* pOutput = outputSequence.getArray(); + + for( i=0; i<nNumPolies; ++i ) + { + pOutput[i] = pointSequenceFromB2DPolygon( rPolyPoly.getB2DPolygon(i) ); + } + + return outputSequence; + } + + uno::Reference< rendering::XPolyPolygon2D > xPolyPolygonFromB2DPolygon( const uno::Reference< rendering::XGraphicDevice >& xGraphicDevice, + const ::basegfx::B2DPolygon& rPoly ) + { + uno::Reference< rendering::XPolyPolygon2D > xRes; + + if( !xGraphicDevice.is() ) + return xRes; + + if( rPoly.areControlPointsUsed() ) + { + uno::Sequence< uno::Sequence< geometry::RealBezierSegment2D > > outputSequence{ bezierSequenceFromB2DPolygon( rPoly )}; + + xRes = xGraphicDevice->createCompatibleBezierPolyPolygon( outputSequence ); + } + else + { + uno::Sequence< uno::Sequence< geometry::RealPoint2D > > outputSequence{ + pointSequenceFromB2DPolygon( rPoly )}; + + xRes = xGraphicDevice->createCompatibleLinePolyPolygon( outputSequence ); + } + + if( xRes.is() && rPoly.isClosed() ) + xRes->setClosed( 0, true ); + + return xRes; + } + + uno::Reference< rendering::XPolyPolygon2D > xPolyPolygonFromB2DPolyPolygon( const uno::Reference< rendering::XGraphicDevice >& xGraphicDevice, + const ::basegfx::B2DPolyPolygon& rPolyPoly ) + { + uno::Reference< rendering::XPolyPolygon2D > xRes; + + if( !xGraphicDevice.is() ) + return xRes; + + const sal_uInt32 nNumPolies( rPolyPoly.count() ); + sal_uInt32 i; + + if( rPolyPoly.areControlPointsUsed() ) + { + xRes = xGraphicDevice->createCompatibleBezierPolyPolygon( + bezierSequenceSequenceFromB2DPolyPolygon( rPolyPoly ) ); + } + else + { + xRes = xGraphicDevice->createCompatibleLinePolyPolygon( + pointSequenceSequenceFromB2DPolyPolygon( rPolyPoly ) ); + } + + for( i=0; i<nNumPolies; ++i ) + { + xRes->setClosed( i, rPolyPoly.getB2DPolygon(i).isClosed() ); + } + + return xRes; + } + + ::basegfx::B2DPolygon polygonFromPoint2DSequence( const uno::Sequence< geometry::RealPoint2D >& points ) + { + const sal_Int32 nCurrSize( points.getLength() ); + + ::basegfx::B2DPolygon aPoly; + + for( sal_Int32 nCurrPoint=0; nCurrPoint<nCurrSize; ++nCurrPoint ) + aPoly.append( b2DPointFromRealPoint2D( points[nCurrPoint] ) ); + + return aPoly; + } + + ::basegfx::B2DPolyPolygon polyPolygonFromPoint2DSequenceSequence( const uno::Sequence< uno::Sequence< geometry::RealPoint2D > >& points ) + { + ::basegfx::B2DPolyPolygon aRes; + + for( const auto & p : points ) + { + aRes.append( polygonFromPoint2DSequence( p ) ); + } + + return aRes; + } + + ::basegfx::B2DPolygon polygonFromBezier2DSequence( const uno::Sequence< geometry::RealBezierSegment2D >& curves ) + { + const sal_Int32 nSize(curves.getLength()); + basegfx::B2DPolygon aRetval; + + if(nSize) + { + // prepare start with providing a start point. Use the first point from + // the sequence for this + const geometry::RealBezierSegment2D& rFirstSegment(curves[0]); // #i79917# first segment, not last + aRetval.append(basegfx::B2DPoint(rFirstSegment.Px, rFirstSegment.Py)); + + for(sal_Int32 a(0); a < nSize; a++) + { + const geometry::RealBezierSegment2D& rCurrSegment(curves[a]); + const geometry::RealBezierSegment2D& rNextSegment(curves[(a + 1) % nSize]); + + // append curved edge with the control points and the next point + aRetval.appendBezierSegment( + basegfx::B2DPoint(rCurrSegment.C1x, rCurrSegment.C1y), + basegfx::B2DPoint(rCurrSegment.C2x, rCurrSegment.C2y), // #i79917# Argh! An x for an y!! + basegfx::B2DPoint(rNextSegment.Px, rNextSegment.Py)); + } + + // rescue the control point and remove the now double-added point + aRetval.setPrevControlPoint(0, aRetval.getPrevControlPoint(aRetval.count() - 1)); + aRetval.remove(aRetval.count() - 1); + } + + return aRetval; + } + + ::basegfx::B2DPolyPolygon polyPolygonFromBezier2DSequenceSequence( const uno::Sequence< uno::Sequence< geometry::RealBezierSegment2D > >& curves ) + { + ::basegfx::B2DPolyPolygon aRes; + + for( const auto & c : curves ) + { + aRes.append( polygonFromBezier2DSequence( c ) ); + } + + return aRes; + } + + ::basegfx::B2DPolyPolygon b2DPolyPolygonFromXPolyPolygon2D( const uno::Reference< rendering::XPolyPolygon2D >& xPoly ) + { + ::basegfx::unotools::UnoPolyPolygon* pPolyImpl = + dynamic_cast< ::basegfx::unotools::UnoPolyPolygon* >( xPoly.get() ); + + if( pPolyImpl ) + { + return pPolyImpl->getPolyPolygon(); + } + else + { + // not a known implementation object - try data source + // interfaces + const sal_Int32 nPolys( xPoly->getNumberOfPolygons() ); + + uno::Reference< rendering::XBezierPolyPolygon2D > xBezierPoly( + xPoly, + uno::UNO_QUERY ); + + if( xBezierPoly.is() ) + { + return ::basegfx::unotools::polyPolygonFromBezier2DSequenceSequence( + xBezierPoly->getBezierSegments( 0, + nPolys, + 0, + -1 ) ); + } + else + { + uno::Reference< rendering::XLinePolyPolygon2D > xLinePoly( + xPoly, + uno::UNO_QUERY ); + + // no implementation class and no data provider + // found - contract violation. + if( !xLinePoly.is() ) + { + throw lang::IllegalArgumentException( + "basegfx::unotools::b2DPolyPolygonFromXPolyPolygon2D(): Invalid input" + "poly-polygon, cannot retrieve vertex data", + uno::Reference< uno::XInterface >(), + 0 ); + } + + return ::basegfx::unotools::polyPolygonFromPoint2DSequenceSequence( + xLinePoly->getPoints( 0, + nPolys, + 0, + -1 )); + } + } + } + + ::basegfx::B2DHomMatrix& homMatrixFromAffineMatrix( ::basegfx::B2DHomMatrix& output, + const geometry::AffineMatrix2D& input ) + { + // ensure last row is [0,0,1] (and optimized away) + output.identity(); + + output.set(0,0, input.m00); + output.set(0,1, input.m01); + output.set(0,2, input.m02); + output.set(1,0, input.m10); + output.set(1,1, input.m11); + output.set(1,2, input.m12); + + return output; + } + + ::basegfx::B3DHomMatrix homMatrixFromAffineMatrix3D( const ::css::geometry::AffineMatrix3D& input ) + { + ::basegfx::B3DHomMatrix output; + + output.set(0,0, input.m00); + output.set(0,1, input.m01); + output.set(0,2, input.m02); + output.set(0,3, input.m03); + + output.set(1,0, input.m10); + output.set(1,1, input.m11); + output.set(1,2, input.m12); + output.set(1,3, input.m13); + + output.set(2,0, input.m20); + output.set(2,1, input.m21); + output.set(2,2, input.m22); + output.set(2,3, input.m23); + + return output; + } + + geometry::AffineMatrix2D& affineMatrixFromHomMatrix( geometry::AffineMatrix2D& output, + const ::basegfx::B2DHomMatrix& input) + { + output.m00 = input.get(0,0); + output.m01 = input.get(0,1); + output.m02 = input.get(0,2); + output.m10 = input.get(1,0); + output.m11 = input.get(1,1); + output.m12 = input.get(1,2); + + return output; + } + + geometry::AffineMatrix3D& affineMatrixFromHomMatrix3D( + geometry::AffineMatrix3D& output, + const ::basegfx::B3DHomMatrix& input) + { + output.m00 = input.get(0,0); + output.m01 = input.get(0,1); + output.m02 = input.get(0,2); + output.m03 = input.get(0,3); + + output.m10 = input.get(1,0); + output.m11 = input.get(1,1); + output.m12 = input.get(1,2); + output.m13 = input.get(1,3); + + output.m20 = input.get(2,0); + output.m21 = input.get(2,1); + output.m22 = input.get(2,2); + output.m23 = input.get(2,3); + + return output; + } + + geometry::RealSize2D size2DFromB2DSize(const ::basegfx::B2DSize& rSize) + { + return geometry::RealSize2D(rSize.getWidth(), rSize.getHeight()); + } + + geometry::RealPoint2D point2DFromB2DPoint( const ::basegfx::B2DPoint& rPoint ) + { + return geometry::RealPoint2D( rPoint.getX(), + rPoint.getY() ); + } + + geometry::RealRectangle2D rectangle2DFromB2DRectangle( const ::basegfx::B2DRange& rRect ) + { + return geometry::RealRectangle2D( rRect.getMinX(), + rRect.getMinY(), + rRect.getMaxX(), + rRect.getMaxY() ); + } + + geometry::RealRectangle3D rectangle3DFromB3DRectangle( const ::basegfx::B3DRange& rRect ) + { + return geometry::RealRectangle3D( rRect.getMinX(), + rRect.getMinY(), + rRect.getMinZ(), + rRect.getMaxX(), + rRect.getMaxY(), + rRect.getMaxZ()); + } + + ::basegfx::B2DPoint b2DPointFromRealPoint2D( const geometry::RealPoint2D& rPoint ) + { + return ::basegfx::B2DPoint( rPoint.X, + rPoint.Y ); + } + + ::basegfx::B2DRange b2DRectangleFromRealRectangle2D( const geometry::RealRectangle2D& rRect ) + { + return ::basegfx::B2DRange( rRect.X1, + rRect.Y1, + rRect.X2, + rRect.Y2 ); + } + + ::basegfx::B3DRange b3DRectangleFromRealRectangle3D( const geometry::RealRectangle3D& rRect ) + { + return ::basegfx::B3DRange( rRect.X1, + rRect.Y1, + rRect.Z1, + rRect.X2, + rRect.Y2, + rRect.Z2); + } + + geometry::IntegerSize2D integerSize2DFromB2ISize( const ::basegfx::B2ISize& rSize ) + { + return geometry::IntegerSize2D( rSize.getWidth(), + rSize.getHeight() ); + } + + basegfx::B2ISize b2ISizeFromIntegerSize2D( const geometry::IntegerSize2D& rSize ) + { + return basegfx::B2ISize(rSize.Width, rSize.Height); + } + + ::basegfx::B2IRange b2IRectangleFromIntegerRectangle2D( const geometry::IntegerRectangle2D& rRectangle ) + { + return ::basegfx::B2IRange( rRectangle.X1, rRectangle.Y1, + rRectangle.X2, rRectangle.Y2 ); + } + + ::basegfx::B2IRange b2IRectangleFromAwtRectangle( const awt::Rectangle& rRect ) + { + return ::basegfx::B2IRange( rRect.X, + rRect.Y, + rRect.X + rRect.Width, + rRect.Y + rRect.Height ); + } + + ::basegfx::B2IRange b2ISurroundingRangeFromB2DRange( const ::basegfx::B2DRange& rRange ) + { + return ::basegfx::B2IRange( static_cast<sal_Int32>( floor(rRange.getMinX()) ), + static_cast<sal_Int32>( floor(rRange.getMinY()) ), + static_cast<sal_Int32>( ceil(rRange.getMaxX()) ), + static_cast<sal_Int32>( ceil(rRange.getMaxY()) ) ); + } + + ::basegfx::B2DRange b2DSurroundingIntegerRangeFromB2DRange( const ::basegfx::B2DRange& rRange ) + { + return ::basegfx::B2DRange( floor(rRange.getMinX()), + floor(rRange.getMinY()), + ceil(rRange.getMaxX()), + ceil(rRange.getMaxY()) ); + } + +} // namespace + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/basegfx/source/tools/gradienttools.cxx b/basegfx/source/tools/gradienttools.cxx new file mode 100644 index 0000000000..8f3e8ae83c --- /dev/null +++ b/basegfx/source/tools/gradienttools.cxx @@ -0,0 +1,756 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * 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 . + */ + +#include <basegfx/utils/gradienttools.hxx> +#include <basegfx/point/b2dpoint.hxx> +#include <basegfx/range/b2drange.hxx> +#include <basegfx/matrix/b2dhommatrixtools.hxx> +#include <com/sun/star/awt/Gradient2.hpp> +#include <osl/endian.h> + +#include <algorithm> +#include <cmath> + +namespace basegfx +{ + bool ODFGradientInfo::operator==(const ODFGradientInfo& rODFGradientInfo) const + { + return getTextureTransform() == rODFGradientInfo.getTextureTransform() + && getAspectRatio() == rODFGradientInfo.getAspectRatio() + && getRequestedSteps() == rODFGradientInfo.getRequestedSteps(); + } + + const B2DHomMatrix& ODFGradientInfo::getBackTextureTransform() const + { + if(maBackTextureTransform.isIdentity()) + { + const_cast< ODFGradientInfo* >(this)->maBackTextureTransform = getTextureTransform(); + const_cast< ODFGradientInfo* >(this)->maBackTextureTransform.invert(); + } + + return maBackTextureTransform; + } + + /** Most of the setup for linear & axial gradient is the same, except + for the border treatment. Factored out here. + */ + static ODFGradientInfo init1DGradientInfo( + const B2DRange& rTargetRange, + sal_uInt32 nSteps, + double fBorder, + double fAngle, + bool bAxial) + { + B2DHomMatrix aTextureTransform; + + fAngle = -fAngle; + + double fTargetSizeX(rTargetRange.getWidth()); + double fTargetSizeY(rTargetRange.getHeight()); + double fTargetOffsetX(rTargetRange.getMinX()); + double fTargetOffsetY(rTargetRange.getMinY()); + + // add object expansion + const bool bAngleUsed(!fTools::equalZero(fAngle)); + + if(bAngleUsed) + { + const double fAbsCos(fabs(cos(fAngle))); + const double fAbsSin(fabs(sin(fAngle))); + const double fNewX(fTargetSizeX * fAbsCos + fTargetSizeY * fAbsSin); + const double fNewY(fTargetSizeY * fAbsCos + fTargetSizeX * fAbsSin); + + fTargetOffsetX -= (fNewX - fTargetSizeX) / 2.0; + fTargetOffsetY -= (fNewY - fTargetSizeY) / 2.0; + fTargetSizeX = fNewX; + fTargetSizeY = fNewY; + } + + const double fSizeWithoutBorder(1.0 - fBorder); + + if(bAxial) + { + aTextureTransform.scale(1.0, fSizeWithoutBorder * 0.5); + aTextureTransform.translate(0.0, 0.5); + } + else + { + if(!fTools::equal(fSizeWithoutBorder, 1.0)) + { + aTextureTransform.scale(1.0, fSizeWithoutBorder); + aTextureTransform.translate(0.0, fBorder); + } + } + + aTextureTransform.scale(fTargetSizeX, fTargetSizeY); + + // add texture rotate after scale to keep perpendicular angles + if(bAngleUsed) + { + const B2DPoint aCenter(0.5 * fTargetSizeX, 0.5 * fTargetSizeY); + + aTextureTransform *= basegfx::utils::createRotateAroundPoint(aCenter, fAngle); + } + + // add object translate + aTextureTransform.translate(fTargetOffsetX, fTargetOffsetY); + + // prepare aspect for texture + const double fAspectRatio(fTools::equalZero(fTargetSizeY) ? 1.0 : fTargetSizeX / fTargetSizeY); + + return ODFGradientInfo(aTextureTransform, fAspectRatio, nSteps); + } + + /** Most of the setup for radial & ellipsoidal gradient is the same, + except for the border treatment. Factored out here. + */ + static ODFGradientInfo initEllipticalGradientInfo( + const B2DRange& rTargetRange, + const B2DVector& rOffset, + sal_uInt32 nSteps, + double fBorder, + double fAngle, + bool bCircular) + { + B2DHomMatrix aTextureTransform; + + fAngle = -fAngle; + + double fTargetSizeX(rTargetRange.getWidth()); + double fTargetSizeY(rTargetRange.getHeight()); + double fTargetOffsetX(rTargetRange.getMinX()); + double fTargetOffsetY(rTargetRange.getMinY()); + + // add object expansion + if(bCircular) + { + const double fOriginalDiag(std::hypot(fTargetSizeX, fTargetSizeY)); + + fTargetOffsetX -= (fOriginalDiag - fTargetSizeX) / 2.0; + fTargetOffsetY -= (fOriginalDiag - fTargetSizeY) / 2.0; + fTargetSizeX = fOriginalDiag; + fTargetSizeY = fOriginalDiag; + } + else + { + fTargetOffsetX -= ((M_SQRT2 - 1) / 2.0 ) * fTargetSizeX; + fTargetOffsetY -= ((M_SQRT2 - 1) / 2.0 ) * fTargetSizeY; + fTargetSizeX = M_SQRT2 * fTargetSizeX; + fTargetSizeY = M_SQRT2 * fTargetSizeY; + } + + const double fHalfBorder((1.0 - fBorder) * 0.5); + + aTextureTransform.scale(fHalfBorder, fHalfBorder); + aTextureTransform.translate(0.5, 0.5); + aTextureTransform.scale(fTargetSizeX, fTargetSizeY); + + // add texture rotate after scale to keep perpendicular angles + if(!bCircular && !fTools::equalZero(fAngle)) + { + const B2DPoint aCenter(0.5 * fTargetSizeX, 0.5 * fTargetSizeY); + + aTextureTransform *= basegfx::utils::createRotateAroundPoint(aCenter, fAngle); + } + + // add defined offsets after rotation + if(!fTools::equal(0.5, rOffset.getX()) || !fTools::equal(0.5, rOffset.getY())) + { + // use original target size + fTargetOffsetX += (rOffset.getX() - 0.5) * rTargetRange.getWidth(); + fTargetOffsetY += (rOffset.getY() - 0.5) * rTargetRange.getHeight(); + } + + // add object translate + aTextureTransform.translate(fTargetOffsetX, fTargetOffsetY); + + // prepare aspect for texture + const double fAspectRatio(fTargetSizeY == 0.0 ? 1.0 : (fTargetSizeX / fTargetSizeY)); + + return ODFGradientInfo(aTextureTransform, fAspectRatio, nSteps); + } + + /** Setup for rect & square gradient is exactly the same. Factored out + here. + */ + static ODFGradientInfo initRectGradientInfo( + const B2DRange& rTargetRange, + const B2DVector& rOffset, + sal_uInt32 nSteps, + double fBorder, + double fAngle, + bool bSquare) + { + B2DHomMatrix aTextureTransform; + + fAngle = -fAngle; + + double fTargetSizeX(rTargetRange.getWidth()); + double fTargetSizeY(rTargetRange.getHeight()); + double fTargetOffsetX(rTargetRange.getMinX()); + double fTargetOffsetY(rTargetRange.getMinY()); + + // add object expansion + if(bSquare) + { + const double fSquareWidth(std::max(fTargetSizeX, fTargetSizeY)); + + fTargetOffsetX -= (fSquareWidth - fTargetSizeX) / 2.0; + fTargetOffsetY -= (fSquareWidth - fTargetSizeY) / 2.0; + fTargetSizeX = fTargetSizeY = fSquareWidth; + } + + // add object expansion + const bool bAngleUsed(!fTools::equalZero(fAngle)); + + if(bAngleUsed) + { + const double fAbsCos(fabs(cos(fAngle))); + const double fAbsSin(fabs(sin(fAngle))); + const double fNewX(fTargetSizeX * fAbsCos + fTargetSizeY * fAbsSin); + const double fNewY(fTargetSizeY * fAbsCos + fTargetSizeX * fAbsSin); + + fTargetOffsetX -= (fNewX - fTargetSizeX) / 2.0; + fTargetOffsetY -= (fNewY - fTargetSizeY) / 2.0; + fTargetSizeX = fNewX; + fTargetSizeY = fNewY; + } + + const double fHalfBorder((1.0 - fBorder) * 0.5); + + aTextureTransform.scale(fHalfBorder, fHalfBorder); + aTextureTransform.translate(0.5, 0.5); + aTextureTransform.scale(fTargetSizeX, fTargetSizeY); + + // add texture rotate after scale to keep perpendicular angles + if(bAngleUsed) + { + const B2DPoint aCenter(0.5 * fTargetSizeX, 0.5 * fTargetSizeY); + + aTextureTransform *= basegfx::utils::createRotateAroundPoint(aCenter, fAngle); + } + + // add defined offsets after rotation + if(!fTools::equal(0.5, rOffset.getX()) || !fTools::equal(0.5, rOffset.getY())) + { + // use original target size + fTargetOffsetX += (rOffset.getX() - 0.5) * rTargetRange.getWidth(); + fTargetOffsetY += (rOffset.getY() - 0.5) * rTargetRange.getHeight(); + } + + // add object translate + aTextureTransform.translate(fTargetOffsetX, fTargetOffsetY); + + // prepare aspect for texture + const double fAspectRatio(fTargetSizeY == 0.0 ? 1.0 : (fTargetSizeX / fTargetSizeY)); + + return ODFGradientInfo(aTextureTransform, fAspectRatio, nSteps); + } + + namespace utils + { + /* Tooling method to extract data from given BGradient + to ColorStops, doing some corrections, partially based + on given SingleColor */ + void prepareColorStops( + const basegfx::BGradient& rGradient, + BColorStops& rColorStops, + BColor& rSingleColor) + { + if (rGradient.GetColorStops().isSingleColor(rSingleColor)) + { + // when single color, preserve value in rSingleColor + // and clear the ColorStops, done. + rColorStops.clear(); + return; + } + + const bool bAdaptStartEndIntensity(100 != rGradient.GetStartIntens() || 100 != rGradient.GetEndIntens()); + const bool bAdaptBorder(0 != rGradient.GetBorder()); + + if (!bAdaptStartEndIntensity && !bAdaptBorder) + { + // copy unchanged ColorStops & done + rColorStops = rGradient.GetColorStops(); + return; + } + + // prepare a copy to work on + basegfx::BGradient aWorkCopy(rGradient); + + if (bAdaptStartEndIntensity) + { + aWorkCopy.tryToApplyStartEndIntensity(); + + // this can again lead to single color (e.g. both zero, so + // all black), so check again for it + if (aWorkCopy.GetColorStops().isSingleColor(rSingleColor)) + { + rColorStops.clear(); + return; + } + } + + if (bAdaptBorder) + { + aWorkCopy.tryToApplyBorder(); + } + + // extract ColorStops, that's all we need here + rColorStops = aWorkCopy.GetColorStops(); + } + + /* Tooling method to synchronize the given ColorStops. + The intention is that a color GradientStops and an + alpha/transparence GradientStops gets synchronized + for export. */ + void synchronizeColorStops( + BColorStops& rColorStops, + BColorStops& rAlphaStops, + const BColor& rSingleColor, + const BColor& rSingleAlpha) + { + if (rColorStops.empty()) + { + if (rAlphaStops.empty()) + { + // no AlphaStops and no ColorStops + // create two-stop fallbacks for both + rColorStops = BColorStops { + BColorStop(0.0, rSingleColor), + BColorStop(1.0, rSingleColor) }; + rAlphaStops = BColorStops { + BColorStop(0.0, rSingleAlpha), + BColorStop(1.0, rSingleAlpha) }; + } + else + { + // AlphaStops but no ColorStops + // create fallback synched with existing AlphaStops + for (const auto& cand : rAlphaStops) + { + rColorStops.emplace_back(cand.getStopOffset(), rSingleColor); + } + } + + // preparations complete, we are done + return; + } + else if (rAlphaStops.empty()) + { + // ColorStops but no AlphaStops + // create fallback AlphaStops synched with existing ColorStops using SingleAlpha + for (const auto& cand : rColorStops) + { + rAlphaStops.emplace_back(cand.getStopOffset(), rSingleAlpha); + } + + // preparations complete, we are done + return; + } + + // here we have ColorStops and AlphaStops not empty. Check if we need to + // synchronize both or if they are already usable/in a synched state so + // that they have same count and same StopOffsets + bool bNeedToSyncronize(rColorStops.size() != rAlphaStops.size()); + + if (!bNeedToSyncronize) + { + // check for same StopOffsets + BColorStops::const_iterator aCurrColor(rColorStops.begin()); + BColorStops::const_iterator aCurrAlpha(rAlphaStops.begin()); + + while (!bNeedToSyncronize && + aCurrColor != rColorStops.end() && + aCurrAlpha != rAlphaStops.end()) + { + if (fTools::equal(aCurrColor->getStopOffset(), aCurrAlpha->getStopOffset())) + { + aCurrColor++; + aCurrAlpha++; + } + else + { + bNeedToSyncronize = true; + } + } + } + + if (bNeedToSyncronize) + { + // synchronize sizes & StopOffsets + BColorStops::const_iterator aCurrColor(rColorStops.begin()); + BColorStops::const_iterator aCurrAlpha(rAlphaStops.begin()); + BColorStops aNewColor; + BColorStops aNewAlpha; + BColorStops::BColorStopRange aColorStopRange; + BColorStops::BColorStopRange aAlphaStopRange; + bool bRealChange(false); + + do { + const bool bColor(aCurrColor != rColorStops.end()); + const bool bAlpha(aCurrAlpha != rAlphaStops.end()); + + if (bColor && bAlpha) + { + const double fColorOff(aCurrColor->getStopOffset()); + const double fAlphaOff(aCurrAlpha->getStopOffset()); + + if (fTools::less(fColorOff, fAlphaOff)) + { + // copy color, create alpha + aNewColor.emplace_back(fColorOff, aCurrColor->getStopColor()); + aNewAlpha.emplace_back(fColorOff, rAlphaStops.getInterpolatedBColor(fColorOff, 0, aAlphaStopRange)); + bRealChange = true; + aCurrColor++; + } + else if (fTools::more(fColorOff, fAlphaOff)) + { + // copy alpha, create color + aNewColor.emplace_back(fAlphaOff, rColorStops.getInterpolatedBColor(fAlphaOff, 0, aColorStopRange)); + aNewAlpha.emplace_back(fAlphaOff, aCurrAlpha->getStopColor()); + bRealChange = true; + aCurrAlpha++; + } + else + { + // equal: copy both, advance + aNewColor.emplace_back(fColorOff, aCurrColor->getStopColor()); + aNewAlpha.emplace_back(fAlphaOff, aCurrAlpha->getStopColor()); + aCurrColor++; + aCurrAlpha++; + } + } + else if (bColor) + { + const double fColorOff(aCurrColor->getStopOffset()); + aNewAlpha.emplace_back(fColorOff, rAlphaStops.getInterpolatedBColor(fColorOff, 0, aAlphaStopRange)); + aNewColor.emplace_back(fColorOff, aCurrColor->getStopColor()); + bRealChange = true; + aCurrColor++; + } + else if (bAlpha) + { + const double fAlphaOff(aCurrAlpha->getStopOffset()); + aNewColor.emplace_back(fAlphaOff, rColorStops.getInterpolatedBColor(fAlphaOff, 0, aColorStopRange)); + aNewAlpha.emplace_back(fAlphaOff, aCurrAlpha->getStopColor()); + bRealChange = true; + aCurrAlpha++; + } + else + { + // no more input, break do..while loop + break; + } + } + while(true); + + if (bRealChange) + { + // copy on 'real' change, that means data was added. + // This should always be the cease and should have been + // detected as such above, see bNeedToSyncronize + rColorStops = aNewColor; + rAlphaStops = aNewAlpha; // MCGR: tdf#155537 used wrong result here + } + } + } + + sal_uInt32 calculateNumberOfSteps( + sal_uInt32 nRequestedSteps, + const BColor& rStart, + const BColor& rEnd) + { + const sal_uInt32 nMaxSteps(sal_uInt32((rStart.getMaximumDistance(rEnd) * 127.5) + 0.5)); + + if (0 == nRequestedSteps) + { + nRequestedSteps = nMaxSteps; + } + + if(nRequestedSteps > nMaxSteps) + { + nRequestedSteps = nMaxSteps; + } + + return std::max(sal_uInt32(1), nRequestedSteps); + } + + ODFGradientInfo createLinearODFGradientInfo( + const B2DRange& rTargetArea, + sal_uInt32 nSteps, + double fBorder, + double fAngle) + { + return init1DGradientInfo( + rTargetArea, + nSteps, + fBorder, + fAngle, + false); + } + + ODFGradientInfo createAxialODFGradientInfo( + const B2DRange& rTargetArea, + sal_uInt32 nSteps, + double fBorder, + double fAngle) + { + return init1DGradientInfo( + rTargetArea, + nSteps, + fBorder, + fAngle, + true); + } + + ODFGradientInfo createRadialODFGradientInfo( + const B2DRange& rTargetArea, + const B2DVector& rOffset, + sal_uInt32 nSteps, + double fBorder) + { + return initEllipticalGradientInfo( + rTargetArea, + rOffset, + nSteps, + fBorder, + 0.0, + true); + } + + ODFGradientInfo createEllipticalODFGradientInfo( + const B2DRange& rTargetArea, + const B2DVector& rOffset, + sal_uInt32 nSteps, + double fBorder, + double fAngle) + { + return initEllipticalGradientInfo( + rTargetArea, + rOffset, + nSteps, + fBorder, + fAngle, + false); + } + + ODFGradientInfo createSquareODFGradientInfo( + const B2DRange& rTargetArea, + const B2DVector& rOffset, + sal_uInt32 nSteps, + double fBorder, + double fAngle) + { + return initRectGradientInfo( + rTargetArea, + rOffset, + nSteps, + fBorder, + fAngle, + true); + } + + ODFGradientInfo createRectangularODFGradientInfo( + const B2DRange& rTargetArea, + const B2DVector& rOffset, + sal_uInt32 nSteps, + double fBorder, + double fAngle) + { + return initRectGradientInfo( + rTargetArea, + rOffset, + nSteps, + fBorder, + fAngle, + false); + } + + double getLinearGradientAlpha(const B2DPoint& rUV, const ODFGradientInfo& rGradInfo) + { + const B2DPoint aCoor(rGradInfo.getBackTextureTransform() * rUV); + + // Ignore X, this is not needed at all for Y-Oriented gradients + // if(aCoor.getX() < 0.0 || aCoor.getX() > 1.0) + // { + // return 0.0; + // } + + if(aCoor.getY() <= 0.0) + { + return 0.0; // start value for inside + } + + if(aCoor.getY() >= 1.0) + { + return 1.0; // end value for outside + } + + return aCoor.getY(); + } + + double getAxialGradientAlpha(const B2DPoint& rUV, const ODFGradientInfo& rGradInfo) + { + const B2DPoint aCoor(rGradInfo.getBackTextureTransform() * rUV); + + // Ignore X, this is not needed at all for Y-Oriented gradients + //if(aCoor.getX() < 0.0 || aCoor.getX() > 1.0) + //{ + // return 0.0; + //} + + const double fAbsY(fabs(aCoor.getY())); + + if(fAbsY >= 1.0) + { + return 1.0; // use end value when outside in Y + } + + return fAbsY; + } + + double getRadialGradientAlpha(const B2DPoint& rUV, const ODFGradientInfo& rGradInfo) + { + const B2DPoint aCoor(rGradInfo.getBackTextureTransform() * rUV); + + if(aCoor.getX() < -1.0 || aCoor.getX() > 1.0 || aCoor.getY() < -1.0 || aCoor.getY() > 1.0) + { + return 0.0; + } + + return 1.0 - std::hypot(aCoor.getX(), aCoor.getY()); + } + + double getEllipticalGradientAlpha(const B2DPoint& rUV, const ODFGradientInfo& rGradInfo) + { + const B2DPoint aCoor(rGradInfo.getBackTextureTransform() * rUV); + + if(aCoor.getX() < -1.0 || aCoor.getX() > 1.0 || aCoor.getY() < -1.0 || aCoor.getY() > 1.0) + { + return 0.0; + } + + double fAspectRatio(rGradInfo.getAspectRatio()); + double t(1.0); + + // MCGR: Similar to getRectangularGradientAlpha (please + // see there) we need to use aspect ratio here. Due to + // initEllipticalGradientInfo using M_SQRT2 to make this + // gradient look 'nicer' this correction seems not 100% + // correct, but is close enough for now + if(fAspectRatio > 1.0) + { + t = 1.0 - std::hypot(aCoor.getX() / fAspectRatio, aCoor.getY()); + } + else if(fAspectRatio > 0.0) + { + t = 1.0 - std::hypot(aCoor.getX(), aCoor.getY() * fAspectRatio); + } + + return t; + } + + double getSquareGradientAlpha(const B2DPoint& rUV, const ODFGradientInfo& rGradInfo) + { + const B2DPoint aCoor(rGradInfo.getBackTextureTransform() * rUV); + const double fAbsX(fabs(aCoor.getX())); + + if(fAbsX >= 1.0) + { + return 0.0; + } + + const double fAbsY(fabs(aCoor.getY())); + + if(fAbsY >= 1.0) + { + return 0.0; + } + + return 1.0 - std::max(fAbsX, fAbsY); + } + + double getRectangularGradientAlpha(const B2DPoint& rUV, const ODFGradientInfo& rGradInfo) + { + const B2DPoint aCoor(rGradInfo.getBackTextureTransform() * rUV); + double fAbsX(fabs(aCoor.getX())); + + if(fAbsX >= 1.0) + { + return 0.0; + } + + double fAbsY(fabs(aCoor.getY())); + + if(fAbsY >= 1.0) + { + return 0.0; + } + + // MCGR: Visualizations using the texturing method for + // displaying gradients (getBackTextureTransform is + // involved) show wrong results for GradientElliptical + // and GradientRect, this can be best seen when using + // less steps, e.g. just four. This thus has influence + // on cppcanvas (slideshow) and 3D textures, so needs + // to be corrected. + // Missing is to use the aspect ratio of the object + // in this [-1, -1, 1, 1] unified coordinate space + // after getBackTextureTransform is applied. Optically + // in the larger direction of the texturing the color + // step distances are too big *because* we are in that + // unit range now. + // To correct that, a kind of 'limo stretching' needs to + // be applied, adding space around the center + // proportional to the aspect ratio, so the intuitive + // idea would be to do + // + // fAbsX' = ((fAspectRatio - 1) + fAbsX) / fAspectRatio + // + // which scales from the center. This does not work, and + // after some thoughts it's clear why: It's not the + // position that needs to be moved (this cannot be + // changed), but the position *before* that scale has + // to be determined to get the correct, shifted color + // for the already 'new' position. Thus, turn around + // the expression as + // + // fAbsX' * fAspectRatio = fAspectRatio - 1 + fAbsX + // fAbsX' * fAspectRatio - fAspectRatio + 1 = fAbsX + // fAbsX = (fAbsX' - 1) * fAspectRatio + 1 + // + // This works and can even be simply adapted for + // fAspectRatio < 1.0 aka vertical is bigger. + double fAspectRatio(rGradInfo.getAspectRatio()); + if(fAspectRatio > 1.0) + { + fAbsX = ((fAbsX - 1) * fAspectRatio) + 1; + } + else if(fAspectRatio > 0.0) + { + fAbsY = ((fAbsY - 1) / fAspectRatio) + 1; + } + + return 1.0 - std::max(fAbsX, fAbsY); + } + } // namespace utils +} // namespace basegfx + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/basegfx/source/tools/keystoplerp.cxx b/basegfx/source/tools/keystoplerp.cxx new file mode 100644 index 0000000000..e5d0d76304 --- /dev/null +++ b/basegfx/source/tools/keystoplerp.cxx @@ -0,0 +1,90 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * 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 . + */ + +#include <basegfx/utils/keystoplerp.hxx> +#include <com/sun/star/uno/Sequence.hxx> +#include <osl/diagnose.h> + +#include <algorithm> + +static void validateInput(const std::vector<double>& rKeyStops) +{ +#ifdef DBG_UTIL + OSL_ENSURE( rKeyStops.size() > 1, + "KeyStopLerp::KeyStopLerp(): key stop vector must have two entries or more" ); + + // rKeyStops must be sorted in ascending order + for( std::size_t i=1, len=rKeyStops.size(); i<len; ++i ) + { + if( rKeyStops[i-1] > rKeyStops[i] ) + OSL_FAIL( "KeyStopLerp::KeyStopLerp(): time vector is not sorted in ascending order!" ); + } +#else + (void)rKeyStops; +#endif +} + +namespace basegfx::utils +{ + KeyStopLerp::KeyStopLerp( std::vector<double>&& rKeyStops ) : + maKeyStops(std::move(rKeyStops)), + mnLastIndex(0) + { + validateInput(maKeyStops); + } + + KeyStopLerp::KeyStopLerp( const ::css::uno::Sequence<double>& rKeyStops ) : + maKeyStops(rKeyStops.begin(), rKeyStops.end()), + mnLastIndex(0) + { + validateInput(maKeyStops); + } + + KeyStopLerp::ResultType KeyStopLerp::lerp(double fAlpha) const + { + // cached value still okay? + if( maKeyStops.at(mnLastIndex) < fAlpha || + maKeyStops.at(mnLastIndex+1) >= fAlpha ) + { + // nope, find new index + mnLastIndex = std::min<std::ptrdiff_t>( + maKeyStops.size()-2, + // range is ensured by max below + std::max<std::ptrdiff_t>( + 0, + std::distance( maKeyStops.begin(), + std::lower_bound( maKeyStops.begin(), + maKeyStops.end(), + fAlpha )) - 1 )); + } + + // lerp between stop and stop+1 + const double fRawLerp= + (fAlpha-maKeyStops.at(mnLastIndex)) / + (maKeyStops.at(mnLastIndex+1) - maKeyStops.at(mnLastIndex)); + + // clamp to permissible range (input fAlpha might be + // everything) + return ResultType( + mnLastIndex, + std::clamp(fRawLerp,0.0,1.0)); + } +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/basegfx/source/tools/numbertools.cxx b/basegfx/source/tools/numbertools.cxx new file mode 100644 index 0000000000..4248068b7d --- /dev/null +++ b/basegfx/source/tools/numbertools.cxx @@ -0,0 +1,58 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include <basegfx/utils/tools.hxx> +#include <basegfx/matrix/b2dhommatrix.hxx> +#include <basegfx/polygon/b2dpolypolygon.hxx> +#include <basegfx/polygon/b2dpolypolygontools.hxx> + +#include <rtl/strbuf.hxx> +#include <rtl/math.hxx> + +namespace basegfx::utils +{ + B2DPolyPolygon number2PolyPolygon(double fValue, sal_Int32 nTotalDigits, sal_Int32 nDecPlaces, bool bLitSegments) + { + // config here + // { + const double fSpace=0.2; + // } + // config here + + OStringBuffer aNum; + rtl::math::doubleToStringBuffer(aNum, + fValue, + rtl_math_StringFormat_F, + nDecPlaces, '.', + nullptr, ','); + + B2DPolyPolygon aRes; + B2DHomMatrix aMat; + double fCurrX=std::max(nTotalDigits-aNum.getLength(), + sal_Int32(0)) * (1.0+fSpace); + for( sal_Int32 i=0; i<aNum.getLength(); ++i ) + { + B2DPolyPolygon aCurr=createSevenSegmentPolyPolygon(aNum[i], + bLitSegments); + + aMat.identity(); + aMat.translate(fCurrX,0.0); + aCurr.transform(aMat); + + fCurrX += 1.0+fSpace; + + aRes.append(aCurr); + } + + return aRes; + } + +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/basegfx/source/tools/stringconversiontools.cxx b/basegfx/source/tools/stringconversiontools.cxx new file mode 100644 index 0000000000..79b6d60466 --- /dev/null +++ b/basegfx/source/tools/stringconversiontools.cxx @@ -0,0 +1,158 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * 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 . + */ + +#include <stringconversiontools.hxx> +#include <rtl/math.hxx> + +namespace basegfx::internal +{ + void skipSpaces(sal_Int32& io_rPos, + std::u16string_view rStr, + const sal_Int32 nLen) + { + while( io_rPos < nLen && + rStr[io_rPos] == ' ' ) + { + ++io_rPos; + } + } + + static void skipSpacesAndCommas(sal_Int32& io_rPos, + std::u16string_view rStr, + const sal_Int32 nLen) + { + while(io_rPos < nLen + && (rStr[io_rPos] == ' ' || rStr[io_rPos] == ',')) + { + ++io_rPos; + } + } + + static bool getDoubleChar(double& o_fRetval, + sal_Int32& io_rPos, + std::u16string_view rStr) + { + const sal_Int64 nStrSize = rStr.size(); + sal_Unicode aChar = io_rPos < nStrSize ? rStr[io_rPos] : 0; + const sal_Int32 nStartPos = io_rPos; + + // sign + if(aChar == '+' || aChar == '-') + { + aChar = rStr[++io_rPos]; + } + + // numbers before point + while('0' <= aChar && '9' >= aChar) + { + io_rPos++; + aChar = io_rPos < nStrSize ? rStr[io_rPos] : 0; + } + + // point + if(aChar == '.') + { + io_rPos++; + aChar = io_rPos < nStrSize ? rStr[io_rPos] : 0; + } + + // numbers after point + while ('0' <= aChar && '9' >= aChar) + { + io_rPos++; + aChar = io_rPos < nStrSize ? rStr[io_rPos] : 0; + } + + // 'e' + if(aChar == 'e' || aChar == 'E') + { + io_rPos++; + aChar = io_rPos < nStrSize ? rStr[io_rPos] : 0; + + // sign for 'e' + if(aChar == '+' || aChar == '-') + { + io_rPos++; + aChar = io_rPos < nStrSize ? rStr[io_rPos] : 0; + } + + // number for 'e' + while('0' <= aChar && '9' >= aChar) + { + io_rPos++; + aChar = io_rPos < nStrSize ? rStr[io_rPos] : 0; + } + } + + const sal_Int32 nLen = io_rPos - nStartPos; + if(nLen) + { + rStr = rStr.substr(nStartPos, nLen); + rtl_math_ConversionStatus eStatus; + o_fRetval = ::rtl::math::stringToDouble( rStr, + '.', + ',', + &eStatus ); + return ( eStatus == rtl_math_ConversionStatus_Ok ); + } + + return false; + } + + bool importDoubleAndSpaces(double& o_fRetval, + sal_Int32& io_rPos, + std::u16string_view rStr, + const sal_Int32 nLen ) + { + if( !getDoubleChar(o_fRetval, io_rPos, rStr) ) + return false; + + skipSpacesAndCommas(io_rPos, rStr, nLen); + + return true; + } + + bool importFlagAndSpaces(sal_Int32& o_nRetval, + sal_Int32& io_rPos, + std::u16string_view rStr, + const sal_Int32 nLen) + { + sal_Unicode aChar( rStr[io_rPos] ); + + if(aChar == '0') + { + o_nRetval = 0; + ++io_rPos; + } + else if (aChar == '1') + { + o_nRetval = 1; + ++io_rPos; + } + else + return false; + + skipSpacesAndCommas(io_rPos, rStr, nLen); + + return true; + } + +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/basegfx/source/tools/systemdependentdata.cxx b/basegfx/source/tools/systemdependentdata.cxx new file mode 100644 index 0000000000..0d64d9982c --- /dev/null +++ b/basegfx/source/tools/systemdependentdata.cxx @@ -0,0 +1,166 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include <basegfx/utils/systemdependentdata.hxx> +#include <config_fuzzers.h> +#include <math.h> + +namespace basegfx +{ + SystemDependentDataManager::SystemDependentDataManager() + { + } + + SystemDependentDataManager::~SystemDependentDataManager() + { + } +} // namespace basegfx + +namespace basegfx +{ + SystemDependentData::SystemDependentData( + SystemDependentDataManager& rSystemDependentDataManager) + : mrSystemDependentDataManager(rSystemDependentDataManager), + mnCalculatedCycles(0) + { + } + + SystemDependentData::~SystemDependentData() + { + } + + sal_uInt32 SystemDependentData::calculateCombinedHoldCyclesInSeconds() const + { +#if ENABLE_FUZZERS + return 0; +#endif + + if(0 == mnCalculatedCycles) + { + const sal_Int64 nBytes(estimateUsageInBytes()); + + // tdf#129845 as indicator for no need to buffer trivial data, stay at and + // return zero. As border, use 450 bytes. For polygons, this means to buffer + // starting with ca. 50 points (GDIPLUS uses 9 bytes per coordinate). For + // Bitmap data this means to more or less always buffer (as it was before). + // For the future, a more sophisticated differentiation may be added + if(nBytes > 450) + { + // HoldCyclesInSeconds + const sal_uInt32 nSeconds = 60; + + // default is Seconds (minimal is one) + sal_uInt32 nResult(0 == nSeconds ? 1 : nSeconds); + + if(0 != nBytes) + { + // use sqrt to get some curved shape. With a default of 60s we get + // a single second at 3600 byte. To get close to 10mb, multiply by + // a corresponding scaling factor + const double fScaleToMB(3600.0 / (1024.0 * 1024.0 * 10.0)); + + // also use a multiplier to move the start point higher + const double fMultiplierSeconds(10.0); + + // calculate + nResult = static_cast<sal_uInt32>((fMultiplierSeconds * nSeconds) / sqrt(nBytes * fScaleToMB)); + + // minimal value is 1 + if(nResult < 1) + { + nResult = 1; + } + + // maximal value is nSeconds + if(nResult > nSeconds) + { + nResult = nSeconds; + } + } + + // set locally (once, on-demand created, non-zero) + const_cast<SystemDependentData*>(this)->mnCalculatedCycles = nResult; + } + } + + return mnCalculatedCycles; + } + + sal_Int64 SystemDependentData::estimateUsageInBytes() const + { + // default implementation has no idea + return 0; + } +} // namespace basegfx + +namespace basegfx +{ + SystemDependentDataHolder::SystemDependentDataHolder() + { + } + + SystemDependentDataHolder::~SystemDependentDataHolder() + { + for(const auto& candidate : maSystemDependentReferences) + { + basegfx::SystemDependentData_SharedPtr aData(candidate.second.lock()); + + if(aData) + { + aData->getSystemDependentDataManager().endUsage(aData); + } + } + } + + void SystemDependentDataHolder::addOrReplaceSystemDependentData(basegfx::SystemDependentData_SharedPtr& rData) + { + const size_t hash_code(typeid(*rData).hash_code()); + auto result(maSystemDependentReferences.find(hash_code)); + + if(result != maSystemDependentReferences.end()) + { + basegfx::SystemDependentData_SharedPtr aData(result->second.lock()); + + if(aData) + { + aData->getSystemDependentDataManager().endUsage(aData); + } + + maSystemDependentReferences.erase(result); + result = maSystemDependentReferences.end(); + } + + maSystemDependentReferences[hash_code] = rData; + rData->getSystemDependentDataManager().startUsage(rData); + } + + SystemDependentData_SharedPtr SystemDependentDataHolder::getSystemDependentData(size_t hash_code) const + { + basegfx::SystemDependentData_SharedPtr aRetval; + auto result(maSystemDependentReferences.find(hash_code)); + + if(result != maSystemDependentReferences.end()) + { + aRetval = result->second.lock(); + + if(aRetval) + { + aRetval->getSystemDependentDataManager().touchUsage(aRetval); + } + else + { + const_cast< SystemDependentDataHolder* >(this)->maSystemDependentReferences.erase(result); + } + } + + return aRetval; + } +} // namespace basegfx + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/basegfx/source/tools/tools.cxx b/basegfx/source/tools/tools.cxx new file mode 100644 index 0000000000..4a7f2c962a --- /dev/null +++ b/basegfx/source/tools/tools.cxx @@ -0,0 +1,111 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * 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 . + */ + +#include <basegfx/utils/tools.hxx> +#include <basegfx/range/b2drange.hxx> + +#include <algorithm> + +namespace basegfx::utils +{ + namespace + { + double distance( const double& nX, + const double& nY, + const ::basegfx::B2DVector& rNormal, + const double& nC ) + { + return nX*rNormal.getX() + nY*rNormal.getY() - nC; + } + + void moveLineOutsideRect( ::basegfx::B2DPoint& io_rStart, + ::basegfx::B2DPoint& io_rEnd, + const ::basegfx::B2DVector& rMoveDirection, + const ::basegfx::B2DRange& rFitTarget ) + { + // calc c for normal line form equation n x - c = 0 + const double nC( rMoveDirection.scalar( io_rStart ) ); + + // calc maximum orthogonal distance for all four bound + // rect corners to the line + const double nMaxDistance( std::max( + 0.0, + std::max( + distance(rFitTarget.getMinX(), + rFitTarget.getMinY(), + rMoveDirection, + nC), + std::max( + distance(rFitTarget.getMinX(), + rFitTarget.getMaxY(), + rMoveDirection, + nC), + std::max( + distance(rFitTarget.getMaxX(), + rFitTarget.getMinY(), + rMoveDirection, + nC), + distance(rFitTarget.getMaxX(), + rFitTarget.getMaxY(), + rMoveDirection, + nC) ) ) ) ) ); + + // now move line points, such that the bound rect + // points are all either 'on' or on the negative side + // of the half-plane + io_rStart += nMaxDistance*rMoveDirection; + io_rEnd += nMaxDistance*rMoveDirection; + } + } + + void infiniteLineFromParallelogram( ::basegfx::B2DPoint& io_rLeftTop, + ::basegfx::B2DPoint& io_rLeftBottom, + ::basegfx::B2DPoint& io_rRightTop, + ::basegfx::B2DPoint& io_rRightBottom, + const ::basegfx::B2DRange& rFitTarget ) + { + // For the top and bottom border line of the + // parallelogram, we determine the distance to all four + // corner points of the bound rect (tl, tr, bl, br). When + // using the unit normal form for lines (n x - c = 0), and + // choosing n to point 'outwards' the parallelogram, then + // all bound rect corner points having positive distance + // to the line lie outside the extended gradient rect, and + // thus, the corresponding border line must be moved the + // maximum distance outwards. + + // don't use the top and bottom border line direction, and + // calculate the normal from them. Instead, use the + // vertical lines (lt - lb or rt - rb), as they more + // faithfully represent the direction of the + // to-be-generated infinite line + ::basegfx::B2DVector aDirectionVertical( io_rLeftTop - io_rLeftBottom ); + aDirectionVertical.normalize(); + + const ::basegfx::B2DVector aNormalTop( aDirectionVertical ); + const ::basegfx::B2DVector aNormalBottom( -aDirectionVertical ); + + // now extend parallelogram, such that the bound rect + // point are included + moveLineOutsideRect( io_rLeftTop, io_rRightTop, aNormalTop, rFitTarget ); + moveLineOutsideRect( io_rLeftBottom, io_rRightBottom, aNormalBottom, rFitTarget ); + } +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/basegfx/source/tools/unopolypolygon.cxx b/basegfx/source/tools/unopolypolygon.cxx new file mode 100644 index 0000000000..41f1497319 --- /dev/null +++ b/basegfx/source/tools/unopolypolygon.cxx @@ -0,0 +1,446 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * 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 . + */ + +#include <com/sun/star/lang/IllegalArgumentException.hpp> + +#include <basegfx/matrix/b2dhommatrix.hxx> +#include <basegfx/range/b2drange.hxx> +#include <basegfx/point/b2dpoint.hxx> +#include <basegfx/utils/canvastools.hxx> +#include <basegfx/polygon/b2dpolygon.hxx> +#include <basegfx/polygon/b2dpolypolygontools.hxx> +#include <basegfx/utils/unopolypolygon.hxx> +#include <basegfx/matrix/b2dhommatrixtools.hxx> +#include <cppuhelper/supportsservice.hxx> +#include <utility> + +using namespace ::com::sun::star; + +namespace basegfx::unotools +{ + UnoPolyPolygon::UnoPolyPolygon( B2DPolyPolygon aPolyPoly ) : + maPolyPoly(std::move( aPolyPoly )), + meFillRule( rendering::FillRule_EVEN_ODD ) + { + } + + void SAL_CALL UnoPolyPolygon::addPolyPolygon( + const geometry::RealPoint2D& position, + const uno::Reference< rendering::XPolyPolygon2D >& polyPolygon ) + { + std::unique_lock const guard( m_aMutex ); + modifying(); + + // TODO(F1): Correctly fulfill the UNO API + // specification. This will probably result in a vector of + // poly-polygons to be stored in this object. + + const sal_Int32 nPolys( polyPolygon->getNumberOfPolygons() ); + + if( !polyPolygon.is() || !nPolys ) + { + // invalid or empty polygon - nothing to do. + return; + } + + B2DPolyPolygon aSrcPoly; + const UnoPolyPolygon* pSrc( dynamic_cast< UnoPolyPolygon* >(polyPolygon.get()) ); + + // try to extract polygon data from interface. First, + // check whether it's the same implementation object, + // which we can tunnel then. + if( pSrc ) + { + aSrcPoly = pSrc->getPolyPolygon(); + } + else + { + // not a known implementation object - try data source + // interfaces + uno::Reference< rendering::XBezierPolyPolygon2D > xBezierPoly( + polyPolygon, + uno::UNO_QUERY ); + + if( xBezierPoly.is() ) + { + aSrcPoly = unotools::polyPolygonFromBezier2DSequenceSequence( + xBezierPoly->getBezierSegments( 0, + nPolys, + 0, + -1 ) ); + } + else + { + uno::Reference< rendering::XLinePolyPolygon2D > xLinePoly( + polyPolygon, + uno::UNO_QUERY ); + + // no implementation class and no data provider + // found - contract violation. + if( !xLinePoly.is() ) + throw lang::IllegalArgumentException( + "UnoPolyPolygon::addPolyPolygon(): Invalid input " + "poly-polygon, cannot retrieve vertex data", + getXWeak(), 1); + + aSrcPoly = unotools::polyPolygonFromPoint2DSequenceSequence( + xLinePoly->getPoints( 0, + nPolys, + 0, + -1 ) ); + } + } + + const B2DRange aBounds( utils::getRange( aSrcPoly ) ); + const B2DVector aOffset( unotools::b2DPointFromRealPoint2D( position ) - + aBounds.getMinimum() ); + + if( !aOffset.equalZero() ) + { + const B2DHomMatrix aTranslate(utils::createTranslateB2DHomMatrix(aOffset)); + aSrcPoly.transform( aTranslate ); + } + + maPolyPoly.append( aSrcPoly ); + } + + sal_Int32 SAL_CALL UnoPolyPolygon::getNumberOfPolygons() + { + std::unique_lock const guard( m_aMutex ); + return maPolyPoly.count(); + } + + sal_Int32 SAL_CALL UnoPolyPolygon::getNumberOfPolygonPoints( + sal_Int32 polygon ) + { + std::unique_lock const guard( m_aMutex ); + checkIndex( polygon ); + + return maPolyPoly.getB2DPolygon(polygon).count(); + } + + rendering::FillRule SAL_CALL UnoPolyPolygon::getFillRule() + { + std::unique_lock const guard( m_aMutex ); + return meFillRule; + } + + void SAL_CALL UnoPolyPolygon::setFillRule( + rendering::FillRule fillRule ) + { + std::unique_lock const guard( m_aMutex ); + modifying(); + + meFillRule = fillRule; + } + + sal_Bool SAL_CALL UnoPolyPolygon::isClosed( + sal_Int32 index ) + { + std::unique_lock const guard( m_aMutex ); + checkIndex( index ); + + return maPolyPoly.getB2DPolygon(index).isClosed(); + } + + void SAL_CALL UnoPolyPolygon::setClosed( + sal_Int32 index, + sal_Bool closedState ) + { + std::unique_lock const guard( m_aMutex ); + modifying(); + + if( index == -1 ) + { + // set all + maPolyPoly.setClosed( closedState ); + } + else + { + checkIndex( index ); + + // fetch referenced polygon, change state + B2DPolygon aTmp( maPolyPoly.getB2DPolygon(index) ); + aTmp.setClosed( closedState ); + + // set back to container + maPolyPoly.setB2DPolygon( index, aTmp ); + } + } + + uno::Sequence< uno::Sequence< geometry::RealPoint2D > > SAL_CALL UnoPolyPolygon::getPoints( + sal_Int32 nPolygonIndex, + sal_Int32 nNumberOfPolygons, + sal_Int32 nPointIndex, + sal_Int32 nNumberOfPoints ) + { + return unotools::pointSequenceSequenceFromB2DPolyPolygon( + getSubsetPolyPolygon( nPolygonIndex, + nNumberOfPolygons, + nPointIndex, + nNumberOfPoints ) ); + } + + void SAL_CALL UnoPolyPolygon::setPoints( + const uno::Sequence< uno::Sequence< geometry::RealPoint2D > >& points, + sal_Int32 nPolygonIndex ) + { + std::unique_lock const guard( m_aMutex ); + modifying(); + + const B2DPolyPolygon& rNewPolyPoly( + unotools::polyPolygonFromPoint2DSequenceSequence( points ) ); + + if( nPolygonIndex == -1 ) + { + maPolyPoly = rNewPolyPoly; + } + else + { + checkIndex( nPolygonIndex ); + + maPolyPoly.insert( nPolygonIndex, rNewPolyPoly ); + } + } + + geometry::RealPoint2D SAL_CALL UnoPolyPolygon::getPoint( + sal_Int32 nPolygonIndex, + sal_Int32 nPointIndex ) + { + std::unique_lock const guard( m_aMutex ); + checkIndex( nPolygonIndex ); + + const B2DPolygon& rPoly( maPolyPoly.getB2DPolygon( nPolygonIndex ) ); + + if( nPointIndex < 0 || o3tl::make_unsigned(nPointIndex) >= rPoly.count() ) + throw lang::IndexOutOfBoundsException(); + + return unotools::point2DFromB2DPoint( rPoly.getB2DPoint( nPointIndex ) ); + } + + void SAL_CALL UnoPolyPolygon::setPoint( + const geometry::RealPoint2D& point, + sal_Int32 nPolygonIndex, + sal_Int32 nPointIndex ) + { + std::unique_lock const guard( m_aMutex ); + checkIndex( nPolygonIndex ); + modifying(); + + B2DPolygon aPoly( maPolyPoly.getB2DPolygon( nPolygonIndex ) ); + + if( nPointIndex < 0 || o3tl::make_unsigned(nPointIndex) >= aPoly.count() ) + throw lang::IndexOutOfBoundsException(); + + aPoly.setB2DPoint( nPointIndex, + unotools::b2DPointFromRealPoint2D( point ) ); + maPolyPoly.setB2DPolygon( nPolygonIndex, aPoly ); + } + + uno::Sequence< uno::Sequence< geometry::RealBezierSegment2D > > SAL_CALL UnoPolyPolygon::getBezierSegments( + sal_Int32 nPolygonIndex, + sal_Int32 nNumberOfPolygons, + sal_Int32 nPointIndex, + sal_Int32 nNumberOfPoints ) + { + return unotools::bezierSequenceSequenceFromB2DPolyPolygon( + getSubsetPolyPolygon( nPolygonIndex, + nNumberOfPolygons, + nPointIndex, + nNumberOfPoints ) ); + } + + void SAL_CALL UnoPolyPolygon::setBezierSegments( + const uno::Sequence< uno::Sequence< geometry::RealBezierSegment2D > >& points, + sal_Int32 nPolygonIndex ) + { + std::unique_lock const guard( m_aMutex ); + modifying(); + const B2DPolyPolygon& rNewPolyPoly( + unotools::polyPolygonFromBezier2DSequenceSequence( points ) ); + + if( nPolygonIndex == -1 ) + { + maPolyPoly = rNewPolyPoly; + } + else + { + checkIndex( nPolygonIndex ); + + maPolyPoly.insert( nPolygonIndex, rNewPolyPoly ); + } + } + + geometry::RealBezierSegment2D SAL_CALL UnoPolyPolygon::getBezierSegment( sal_Int32 nPolygonIndex, + sal_Int32 nPointIndex ) + { + std::unique_lock const guard( m_aMutex ); + checkIndex( nPolygonIndex ); + + const B2DPolygon& rPoly( maPolyPoly.getB2DPolygon( nPolygonIndex ) ); + const sal_uInt32 nPointCount(rPoly.count()); + + if( nPointIndex < 0 || o3tl::make_unsigned(nPointIndex) >= nPointCount ) + throw lang::IndexOutOfBoundsException(); + + const B2DPoint& rPt( rPoly.getB2DPoint( nPointIndex ) ); + const B2DPoint& rCtrl0( rPoly.getNextControlPoint(nPointIndex) ); + const B2DPoint& rCtrl1( rPoly.getPrevControlPoint((nPointIndex + 1) % nPointCount) ); + + return geometry::RealBezierSegment2D( rPt.getX(), + rPt.getY(), + rCtrl0.getX(), + rCtrl0.getY(), + rCtrl1.getX(), + rCtrl1.getY() ); + } + + void SAL_CALL UnoPolyPolygon::setBezierSegment( const geometry::RealBezierSegment2D& segment, + sal_Int32 nPolygonIndex, + sal_Int32 nPointIndex ) + { + std::unique_lock const guard( m_aMutex ); + checkIndex( nPolygonIndex ); + modifying(); + + B2DPolygon aPoly( maPolyPoly.getB2DPolygon( nPolygonIndex ) ); + const sal_uInt32 nPointCount(aPoly.count()); + + if( nPointIndex < 0 || o3tl::make_unsigned(nPointIndex) >= nPointCount ) + throw lang::IndexOutOfBoundsException(); + + aPoly.setB2DPoint( nPointIndex, + B2DPoint( segment.Px, + segment.Py ) ); + aPoly.setNextControlPoint(nPointIndex, + B2DPoint(segment.C1x, segment.C1y)); + aPoly.setPrevControlPoint((nPointIndex + 1) % nPointCount, + B2DPoint(segment.C2x, segment.C2y)); + + maPolyPoly.setB2DPolygon( nPolygonIndex, aPoly ); + } + + B2DPolyPolygon UnoPolyPolygon::getSubsetPolyPolygon( + sal_Int32 nPolygonIndex, + sal_Int32 nNumberOfPolygons, + sal_Int32 nPointIndex, + sal_Int32 nNumberOfPoints ) const + { + std::unique_lock const guard( m_aMutex ); + checkIndex( nPolygonIndex ); + + const sal_Int32 nPolyCount( maPolyPoly.count() ); + + // check for "full polygon" case + if( !nPolygonIndex && + !nPointIndex && + nNumberOfPolygons == nPolyCount && + nNumberOfPoints == -1 ) + { + return maPolyPoly; + } + + B2DPolyPolygon aSubsetPoly; + + // create temporary polygon (as an extract from maPoly, + // which contains the requested subset) + for( sal_Int32 i=nPolygonIndex; i<nNumberOfPolygons; ++i ) + { + checkIndex(i); + + const B2DPolygon& rCurrPoly( maPolyPoly.getB2DPolygon(i) ); + + sal_Int32 nFirstPoint(0); + sal_Int32 nLastPoint(nPolyCount-1); + + if( nPointIndex && i==nPolygonIndex ) + { + // very first polygon - respect nPointIndex, if + // not zero + + // empty polygon - impossible to specify _any_ + // legal value except 0 here! + if( !nPolyCount) + throw lang::IndexOutOfBoundsException(); + + nFirstPoint = nPointIndex; + } + + if( i==nNumberOfPolygons-1 && nNumberOfPoints != -1 ) + { + // very last polygon - respect nNumberOfPoints + + // empty polygon - impossible to specify _any_ + // legal value except -1 here! + if( !nPolyCount ) + throw lang::IndexOutOfBoundsException(); + + nLastPoint = nFirstPoint+nNumberOfPoints; + } + + if( !nPolyCount ) + { + // empty polygon - index checks already performed + // above, now simply append empty polygon + aSubsetPoly.append( rCurrPoly ); + } + else + { + if( nFirstPoint < 0 || nFirstPoint >= nPolyCount ) + throw lang::IndexOutOfBoundsException(); + + if( nLastPoint < 0 || nLastPoint >= nPolyCount ) + throw lang::IndexOutOfBoundsException(); + + B2DPolygon aTmp; + for( sal_Int32 j=nFirstPoint; j<nLastPoint; ++j ) + aTmp.append( rCurrPoly.getB2DPoint(j) ); + + aSubsetPoly.append( aTmp ); + } + } + + return aSubsetPoly; + } + + OUString SAL_CALL UnoPolyPolygon::getImplementationName() + { + return "gfx::internal::UnoPolyPolygon"; + } + + sal_Bool SAL_CALL UnoPolyPolygon::supportsService( const OUString& ServiceName ) + { + return cppu::supportsService(this, ServiceName); + } + + uno::Sequence< OUString > SAL_CALL UnoPolyPolygon::getSupportedServiceNames() + { + return { "com.sun.star.rendering.PolyPolygon2D" }; + } + + B2DPolyPolygon UnoPolyPolygon::getPolyPolygon() const + { + std::unique_lock const guard( m_aMutex ); + + return maPolyPoly; + } + +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/basegfx/source/tools/zoomtools.cxx b/basegfx/source/tools/zoomtools.cxx new file mode 100644 index 0000000000..dd4c7a6cbb --- /dev/null +++ b/basegfx/source/tools/zoomtools.cxx @@ -0,0 +1,113 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include <basegfx/utils/zoomtools.hxx> + +namespace basegfx::zoomtools +{ + +/** 2^(1/6) as the default step + + This ensures (unless the rounding is used) that 6 steps lead + to double / half zoom level. +*/ +const double ZOOM_FACTOR = 1.12246205; + +/** +* Round a value against a specified multiple. Values below half +* of the multiple are rounded down and all others are rounded up. +* +* @param nCurrent current value +* @param nMultiple multiple against which the current value is rounded +*/ +static sal_uInt16 roundMultiple(sal_uInt16 nCurrent, int nMultiple) +{ + // round zoom to a multiple of nMultiple + return (( nCurrent + nMultiple / 2 ) - ( nCurrent + nMultiple / 2 ) % nMultiple); +} + +/** +* Convert geometric progression results into more common values by +* rounding them against certain multiples depending on the size. +* Beginning with 50 the multiple is 5, with 100, 10, and so on. +* +* @param nCurrent current zoom factor +*/ +static sal_uInt16 roundZoom(double nCurrent) +{ + // convert nCurrent properly to int + sal_uInt16 nNew = nCurrent + 0.5; + + // round to more common numbers above 50 + if (nNew > 1000) { + nNew = roundMultiple(nNew, 100); + } else if ( nNew > 500 ) { + nNew = roundMultiple(nNew, 50); + } else if ( nNew > 100 ) { + nNew = roundMultiple(nNew, 10); + } else if ( nNew > 50 ) { + nNew = roundMultiple(nNew, 5); + } + + return nNew; +} + +/** +* Make sure that a certain step isn't skipped during the zooming +* progress. +* +* @param nCurrent current zoom factor +* @param nPrevious previous zoom factor +* @param nStep step which shouldn't be skipped +*/ +static sal_uInt16 enforceStep(sal_uInt16 nCurrent, sal_uInt16 nPrevious, unsigned int nStep) +{ + if ((( nCurrent > nStep ) && ( nPrevious < nStep )) + || (( nCurrent < nStep ) && ( nPrevious > nStep ))) + return nStep; + else + return nCurrent; +} + +/** +* Increasing the zoom level. +* +* @param nCurrent current zoom factor +*/ +sal_uInt16 zoomIn(sal_uInt16 nCurrent) +{ + sal_uInt16 nNew = roundZoom( nCurrent * ZOOM_FACTOR ); + // make sure some values are not skipped + nNew = enforceStep(nNew, nCurrent, 200); + nNew = enforceStep(nNew, nCurrent, 100); + nNew = enforceStep(nNew, nCurrent, 75); + nNew = enforceStep(nNew, nCurrent, 50); + nNew = enforceStep(nNew, nCurrent, 25); + return nNew; +} + +/** +* Decreasing the zoom level. +* +* @param nCurrent current zoom factor +*/ +sal_uInt16 zoomOut(sal_uInt16 nCurrent) +{ + sal_uInt16 nNew = roundZoom( nCurrent / ZOOM_FACTOR ); + // make sure some values are not skipped + nNew = enforceStep(nNew, nCurrent, 200); + nNew = enforceStep(nNew, nCurrent, 100); + nNew = enforceStep(nNew, nCurrent, 75); + nNew = enforceStep(nNew, nCurrent, 50); + nNew = enforceStep(nNew, nCurrent, 25); + return nNew; +} +} // namespace + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/basegfx/source/tuple/b2dtuple.cxx b/basegfx/source/tuple/b2dtuple.cxx new file mode 100644 index 0000000000..18a93f8d73 --- /dev/null +++ b/basegfx/source/tuple/b2dtuple.cxx @@ -0,0 +1,43 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * 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 . + */ + +#include <basegfx/tuple/b2dtuple.hxx> +#include <basegfx/tuple/b2ituple.hxx> +#include <basegfx/numeric/ftools.hxx> + +namespace basegfx +{ + const B2DTuple& B2DTuple::getEmptyTuple() + { + static B2DTuple const singleton; + return singleton; + } + + B2DTuple::B2DTuple(const B2ITuple& rTup) + : Tuple2D(rTup.getX(), rTup.getY()) + {} + + B2ITuple fround(const B2DTuple& rTup) + { + return B2ITuple(fround(rTup.getX()), fround(rTup.getY())); + } + +} // end of namespace basegfx + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/basegfx/source/tuple/b3dtuple.cxx b/basegfx/source/tuple/b3dtuple.cxx new file mode 100644 index 0000000000..73e9fd042a --- /dev/null +++ b/basegfx/source/tuple/b3dtuple.cxx @@ -0,0 +1,37 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * 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 . + */ + +#include <basegfx/tuple/b3dtuple.hxx> +#include <basegfx/tuple/b3ituple.hxx> + +namespace basegfx +{ + const B3DTuple& B3DTuple::getEmptyTuple() + { + static B3DTuple const singleton; + return singleton; + } + + B3ITuple fround(const B3DTuple& rTup) + { + return B3ITuple(fround(rTup.getX()), fround(rTup.getY()), fround(rTup.getZ())); + } +} // end of namespace basegfx + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/basegfx/source/vector/b2dvector.cxx b/basegfx/source/vector/b2dvector.cxx new file mode 100644 index 0000000000..85ec6ca811 --- /dev/null +++ b/basegfx/source/vector/b2dvector.cxx @@ -0,0 +1,184 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * 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 . + */ + +#include <basegfx/vector/b2dvector.hxx> +#include <basegfx/matrix/b2dhommatrix.hxx> +#include <basegfx/numeric/ftools.hxx> + +namespace basegfx +{ + B2DVector& B2DVector::normalize() + { + double fLen(scalar(*this)); + + if(fTools::equalZero(fLen)) + { + mnX = 0.0; + mnY = 0.0; + } + else + { + const double fOne(1.0); + + if(!fTools::equal(fOne, fLen)) + { + fLen = sqrt(fLen); + + if(!fTools::equalZero(fLen)) + { + mnX /= fLen; + mnY /= fLen; + } + } + } + + return *this; + } + + double B2DVector::getLength() const + { + if(fTools::equalZero(mnX)) + { + return fabs(mnY); + } + else if(fTools::equalZero(mnY)) + { + return fabs(mnX); + } + + return hypot( mnX, mnY ); + } + + double B2DVector::angle( const B2DVector& rVec ) const + { + return atan2(mnX * rVec.getY() - mnY * rVec.getX(), + mnX * rVec.getX() + mnY * rVec.getY()); + } + + const B2DVector& B2DVector::getEmptyVector() + { + return static_cast<const B2DVector&>( B2DTuple::getEmptyTuple() ); + } + + B2DVector& B2DVector::operator*=( const B2DHomMatrix& rMat ) + { + const double fTempX( rMat.get(0,0)*mnX + + rMat.get(0,1)*mnY ); + const double fTempY( rMat.get(1,0)*mnX + + rMat.get(1,1)*mnY ); + mnX = fTempX; + mnY = fTempY; + + return *this; + } + + B2DVector& B2DVector::setLength(double fLen) + { + double fLenNow(scalar(*this)); + + if(!fTools::equalZero(fLenNow)) + { + const double fOne(1.0); + + if(!fTools::equal(fOne, fLenNow)) + { + fLen /= sqrt(fLenNow); + } + + mnX *= fLen; + mnY *= fLen; + } + + return *this; + } + + bool areParallel( const B2DVector& rVecA, const B2DVector& rVecB ) + { + const double fValA(rVecA.getX() * rVecB.getY()); + const double fValB(rVecA.getY() * rVecB.getX()); + + return fTools::equal(fValA, fValB); + } + + B2VectorOrientation getOrientation( const B2DVector& rVecA, const B2DVector& rVecB ) + { + double fVal(rVecA.getX() * rVecB.getY() - rVecA.getY() * rVecB.getX()); + + if(fTools::equalZero(fVal)) + { + return B2VectorOrientation::Neutral; + } + + if(fVal > 0.0) + { + return B2VectorOrientation::Positive; + } + else + { + return B2VectorOrientation::Negative; + } + } + + B2DVector getPerpendicular( const B2DVector& rNormalizedVec ) + { + B2DVector aPerpendicular(-rNormalizedVec.getY(), rNormalizedVec.getX()); + return aPerpendicular; + } + + B2DVector getNormalizedPerpendicular( const B2DVector& rVec ) + { + B2DVector aPerpendicular(rVec); + aPerpendicular.normalize(); + const double aTemp(-aPerpendicular.getY()); + aPerpendicular.setY(aPerpendicular.getX()); + aPerpendicular.setX(aTemp); + return aPerpendicular; + } + + B2DVector operator*( const B2DHomMatrix& rMat, const B2DVector& rVec ) + { + B2DVector aRes( rVec ); + aRes *= rMat; + return aRes; + } + + B2VectorContinuity getContinuity(const B2DVector& rBackVector, const B2DVector& rForwardVector ) + { + if(rBackVector.equalZero() || rForwardVector.equalZero()) + { + return B2VectorContinuity::NONE; + } + + if(fTools::equal(rBackVector.getX(), -rForwardVector.getX()) && fTools::equal(rBackVector.getY(), -rForwardVector.getY())) + { + // same direction and same length -> C2 + return B2VectorContinuity::C2; + } + + if(areParallel(rBackVector, rForwardVector) && rBackVector.scalar(rForwardVector) < 0.0) + { + // parallel and opposite direction -> C1 + return B2VectorContinuity::C1; + } + + return B2VectorContinuity::NONE; + } +} // end of namespace basegfx + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/basegfx/source/vector/b2ivector.cxx b/basegfx/source/vector/b2ivector.cxx new file mode 100644 index 0000000000..5b45871a39 --- /dev/null +++ b/basegfx/source/vector/b2ivector.cxx @@ -0,0 +1,65 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * 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 . + */ + +#include <basegfx/vector/b2ivector.hxx> +#include <basegfx/matrix/b2dhommatrix.hxx> +#include <basegfx/numeric/ftools.hxx> + +namespace basegfx +{ + B2IVector& B2IVector::operator=( const ::basegfx::B2ITuple& rVec ) + { + mnX = rVec.getX(); + mnY = rVec.getY(); + return *this; + } + + B2IVector& B2IVector::operator*=( const B2DHomMatrix& rMat ) + { + mnX = fround( rMat.get(0,0)*mnX + + rMat.get(0,1)*mnY ); + mnY = fround( rMat.get(1,0)*mnX + + rMat.get(1,1)*mnY ); + + return *this; + } + + B2IVector& B2IVector::setLength(double fLen) + { + double fLenNow(scalar(*this)); + + if(!::basegfx::fTools::equalZero(fLenNow)) + { + const double fOne(1.0); + + if(!::basegfx::fTools::equal(fOne, fLenNow)) + { + fLen /= sqrt(fLenNow); + } + + mnX = fround( mnX*fLen ); + mnY = fround( mnY*fLen ); + } + + return *this; + } + +} // end of namespace basegfx + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/basegfx/source/vector/b3dvector.cxx b/basegfx/source/vector/b3dvector.cxx new file mode 100644 index 0000000000..89bc33c2bd --- /dev/null +++ b/basegfx/source/vector/b3dvector.cxx @@ -0,0 +1,86 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * 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 . + */ + +#include <basegfx/vector/b3dvector.hxx> +#include <basegfx/matrix/b3dhommatrix.hxx> + +namespace basegfx +{ + B3DVector& B3DVector::normalize() + { + double fLen(scalar(*this)); + + if(!::basegfx::fTools::equalZero(fLen)) + { + const double fOne(1.0); + + if(!::basegfx::fTools::equal(fOne, fLen)) + { + fLen = sqrt(fLen); + + mnX /= fLen; + mnY /= fLen; + mnZ /= fLen; + } + } + + return *this; + } + + B3DVector B3DVector::getPerpendicular(const B3DVector& rNormalizedVec) const + { + B3DVector aNew = cross(*this, rNormalizedVec); + aNew.normalize(); + return aNew; + } + + B3DVector& B3DVector::operator*=( const ::basegfx::B3DHomMatrix& rMat ) + { + const double fTempX( rMat.get(0,0)*mnX + rMat.get(0,1)*mnY + rMat.get(0,2)*mnZ ); + const double fTempY( rMat.get(1,0)*mnX + rMat.get(1,1)*mnY + rMat.get(1,2)*mnZ ); + const double fTempZ( rMat.get(2,0)*mnX + rMat.get(2,1)*mnY + rMat.get(2,2)*mnZ ); + mnX = fTempX; + mnY = fTempY; + mnZ = fTempZ; + + return *this; + } + + B3DVector operator*( const ::basegfx::B3DHomMatrix& rMat, const B3DVector& rVec ) + { + B3DVector aRes( rVec ); + aRes *= rMat; + return aRes; + } + + bool areParallel( const B3DVector& rVecA, const B3DVector& rVecB ) + { + // i think fastest is to compare relations, need no square or division + if(!fTools::equal(rVecA.getX() * rVecB.getY(), rVecA.getY() * rVecB.getX())) + return false; + + if(!fTools::equal(rVecA.getX() * rVecB.getZ(), rVecA.getZ() * rVecB.getX())) + return false; + + return fTools::equal(rVecA.getY() * rVecB.getZ(), rVecA.getZ() * rVecB.getY()); + } + +} // end of namespace basegfx + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/basegfx/source/workbench/Makefile b/basegfx/source/workbench/Makefile new file mode 100644 index 0000000000..6218141da1 --- /dev/null +++ b/basegfx/source/workbench/Makefile @@ -0,0 +1,23 @@ +# +# This file is part of the LibreOffice project. +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# This file incorporates work covered by the following license notice: +# +# 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 . +# + +prog : bezierclip.cxx convexhull.cxx + g++ -I. -Wall -g bezierclip.cxx convexhull.cxx -o bezierclip + +.cxx.o: + g++ -c $(LOCALDEFINES) $(CCFLAGS) $< diff --git a/basegfx/source/workbench/bezierclip.cxx b/basegfx/source/workbench/bezierclip.cxx new file mode 100644 index 0000000000..676f239efd --- /dev/null +++ b/basegfx/source/workbench/bezierclip.cxx @@ -0,0 +1,2006 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * 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 . + */ + +#include <iostream> +#include <cassert> +#include <algorithm> +#include <iterator> +#include <vector> +#include <utility> + +#include <math.h> + +#include <bezierclip.hxx> +#include <gauss.hxx> + +// what to test +#define WITH_ASSERTIONS +//#define WITH_CONVEXHULL_TEST +//#define WITH_MULTISUBDIVIDE_TEST +//#define WITH_FATLINE_TEST +//#define WITH_CALCFOCUS_TEST +//#define WITH_SAFEPARAMBASE_TEST +//#define WITH_SAFEPARAMS_TEST +//#define WITH_SAFEPARAM_DETAILED_TEST +//#define WITH_SAFEFOCUSPARAM_CALCFOCUS +//#define WITH_SAFEFOCUSPARAM_TEST +//#define WITH_SAFEFOCUSPARAM_DETAILED_TEST +#define WITH_BEZIERCLIP_TEST + +/* Implementation of the so-called 'Fat-Line Bezier Clipping Algorithm' by Sederberg et al. + * + * Actual reference is: T. W. Sederberg and T Nishita: Curve + * intersection using Bezier clipping. In Computer Aided Design, 22 + * (9), 1990, pp. 538--549 + */ + +/* Misc helper + * =========== + */ +int fallFac( int n, int k ) +{ +#ifdef WITH_ASSERTIONS + assert(n>=k); // "For factorials, n must be greater or equal k" + assert(n>=0); // "For factorials, n must be positive" + assert(k>=0); // "For factorials, k must be positive" +#endif + + int res( 1 ); + + while( k-- && n ) res *= n--; + + return res; +} + +int fac( int n ) +{ + return fallFac(n, n); +} + +/* Bezier fat line clipping part + * ============================= + */ + +void Impl_calcFatLine( FatLine& line, const Bezier& c ) +{ + // Prepare normalized implicit line + // ================================ + + // calculate vector orthogonal to p1-p4: + line.a = -(c.p0.y - c.p3.y); + line.b = (c.p0.x - c.p3.x); + + // normalize + const double len(std::hypot(line.a, line.b)); + if( !tolZero(len) ) + { + line.a /= len; + line.b /= len; + } + + line.c = -(line.a*c.p0.x + line.b*c.p0.y); + + // Determine bounding fat line from it + // =================================== + + // calc control point distances + const double dP2( calcLineDistance(line.a, line.b, line.c, c.p1.x, c.p1.y ) ); + const double dP3( calcLineDistance(line.a, line.b, line.c, c.p2.x, c.p2.y ) ); + + // calc approximate bounding lines to curve (tight bounds are + // possible here, but more expensive to calculate and thus not + // worth the overhead) + if( dP2 * dP3 > 0.0 ) + { + line.dMin = 3.0/4.0 * std::min(0.0, std::min(dP2, dP3)); + line.dMax = 3.0/4.0 * std::max(0.0, std::max(dP2, dP3)); + } + else + { + line.dMin = 4.0/9.0 * std::min(0.0, std::min(dP2, dP3)); + line.dMax = 4.0/9.0 * std::max(0.0, std::max(dP2, dP3)); + } +} + +void Impl_calcBounds( Point2D& leftTop, + Point2D& rightBottom, + const Bezier& c1 ) +{ + leftTop.x = std::min( c1.p0.x, std::min( c1.p1.x, std::min( c1.p2.x, c1.p3.x ) ) ); + leftTop.y = std::min( c1.p0.y, std::min( c1.p1.y, std::min( c1.p2.y, c1.p3.y ) ) ); + rightBottom.x = std::max( c1.p0.x, std::max( c1.p1.x, std::max( c1.p2.x, c1.p3.x ) ) ); + rightBottom.y = std::max( c1.p0.y, std::max( c1.p1.y, std::max( c1.p2.y, c1.p3.y ) ) ); +} + +bool Impl_doBBoxIntersect( const Bezier& c1, + const Bezier& c2 ) +{ + // calc rectangular boxes from c1 and c2 + Point2D lt1; + Point2D rb1; + Point2D lt2; + Point2D rb2; + + Impl_calcBounds( lt1, rb1, c1 ); + Impl_calcBounds( lt2, rb2, c2 ); + + if( std::min(rb1.x, rb2.x) < std::max(lt1.x, lt2.x) || + std::min(rb1.y, rb2.y) < std::max(lt1.y, lt2.y) ) + { + return false; + } + else + { + return true; + } +} + +/* calculates two t's for the given bernstein control polygon: the first is + * the intersection of the min value line with the convex hull from + * the left, the second is the intersection of the max value line with + * the convex hull from the right. + */ +bool Impl_calcSafeParams( double& t1, + double& t2, + const Polygon2D& rPoly, + double lowerYBound, + double upperYBound ) +{ + // need the convex hull of the control polygon, as this is + // guaranteed to completely bound the curve + Polygon2D convHull( convexHull(rPoly) ); + + // init min and max buffers + t1 = 0.0 ; + double currLowerT( 1.0 ); + + t2 = 1.0; + double currHigherT( 0.0 ); + + if( convHull.size() <= 1 ) + return false; // only one point? Then we're done with clipping + + /* now, clip against lower and higher bounds */ + Point2D p0; + Point2D p1; + + bool bIntersection( false ); + + for( Polygon2D::size_type i=0; i<convHull.size(); ++i ) + { + // have to check against convHull.size() segments, as the + // convex hull is, by definition, closed. Thus, for the + // last point, we take the first point as partner. + if( i+1 == convHull.size() ) + { + // close the polygon + p0 = convHull[i]; + p1 = convHull[0]; + } + else + { + p0 = convHull[i]; + p1 = convHull[i+1]; + } + + // is the segment in question within or crossing the + // horizontal band spanned by lowerYBound and upperYBound? If + // not, we've got no intersection. If yes, we maybe don't have + // an intersection, but we've got to update the permissible + // range, nevertheless. This is because inside lying segments + // leads to full range forbidden. + if( (tolLessEqual(p0.y, upperYBound) || tolLessEqual(p1.y, upperYBound)) && + (tolGreaterEqual(p0.y, lowerYBound) || tolGreaterEqual(p1.y, lowerYBound)) ) + { + // calc intersection of convex hull segment with + // one of the horizontal bounds lines + // to optimize a bit, r_x is calculated only in else case + const double r_y( p1.y - p0.y ); + + if( tolZero(r_y) ) + { + // r_y is virtually zero, thus we've got a horizontal + // line. Now check whether we maybe coincide with lower or + // upper horizontal bound line. + if( tolEqual(p0.y, lowerYBound) || + tolEqual(p0.y, upperYBound) ) + { + // yes, simulate intersection then + currLowerT = std::min(currLowerT, std::min(p0.x, p1.x)); + currHigherT = std::max(currHigherT, std::max(p0.x, p1.x)); + } + } + else + { + // check against lower and higher bounds + // ===================================== + const double r_x( p1.x - p0.x ); + + // calc intersection with horizontal dMin line + const double currTLow( (lowerYBound - p0.y) * r_x / r_y + p0.x ); + + // calc intersection with horizontal dMax line + const double currTHigh( (upperYBound - p0.y) * r_x / r_y + p0.x ); + + currLowerT = std::min(currLowerT, std::min(currTLow, currTHigh)); + currHigherT = std::max(currHigherT, std::max(currTLow, currTHigh)); + } + + // set flag that at least one segment is contained or + // intersects given horizontal band. + bIntersection = true; + } + } + +#ifndef WITH_SAFEPARAMBASE_TEST + // limit intersections found to permissible t parameter range + t1 = std::max(0.0, currLowerT); + t2 = std::min(1.0, currHigherT); +#endif + + return bIntersection; +} + +/* calculates two t's for the given bernstein polynomial: the first is + * the intersection of the min value line with the convex hull from + * the left, the second is the intersection of the max value line with + * the convex hull from the right. + * + * The polynomial coefficients c0 to c3 given to this method + * must correspond to t values of 0, 1/3, 2/3 and 1, respectively. + */ +bool Impl_calcSafeParams_clip( double& t1, + double& t2, + const FatLine& bounds, + double c0, + double c1, + double c2, + double c3 ) +{ + /* first of all, determine convex hull of c0-c3 */ + Polygon2D poly(4); + poly[0] = Point2D(0, c0); + poly[1] = Point2D(1.0/3.0, c1); + poly[2] = Point2D(2.0/3.0, c2); + poly[3] = Point2D(1, c3); + +#ifndef WITH_SAFEPARAM_DETAILED_TEST + + return Impl_calcSafeParams( t1, t2, poly, bounds.dMin, bounds.dMax ); + +#else + bool bRet( Impl_calcSafeParams( t1, t2, poly, bounds.dMin, bounds.dMax ) ); + + Polygon2D convHull( convexHull( poly ) ); + + std::cout << "# convex hull testing" << std::endl + << "plot [t=0:1] "; + std::cout << " bez(" + << poly[0].x << "," + << poly[1].x << "," + << poly[2].x << "," + << poly[3].x << ",t),bez(" + << poly[0].y << "," + << poly[1].y << "," + << poly[2].y << "," + << poly[3].y << ",t), " + << "t, " << bounds.dMin << ", " + << "t, " << bounds.dMax << ", " + << t1 << ", t, " + << t2 << ", t, " + << "'-' using ($1):($2) title \"control polygon\" with lp, " + << "'-' using ($1):($2) title \"convex hull\" with lp" << std::endl; + + unsigned int k; + for( k=0; k<poly.size(); ++k ) + { + std::cout << poly[k].x << " " << poly[k].y << std::endl; + } + std::cout << poly[0].x << " " << poly[0].y << std::endl; + std::cout << "e" << std::endl; + + for( k=0; k<convHull.size(); ++k ) + { + std::cout << convHull[k].x << " " << convHull[k].y << std::endl; + } + std::cout << convHull[0].x << " " << convHull[0].y << std::endl; + std::cout << "e" << std::endl; + + return bRet; +#endif +} + +void Impl_deCasteljauAt( Bezier& part1, + Bezier& part2, + const Bezier& input, + double t ) +{ + // deCasteljau bezier arc, scheme is: + + // First row is C_0^n,C_1^n,...,C_n^n + // Second row is P_1^n,...,P_n^n + // etc. + // with P_k^r = (1 - x_s)P_{k-1}^{r-1} + x_s P_k{r-1} + + // this results in: + + // P1 P2 P3 P4 + // L1 P2 P3 R4 + // L2 H R3 + // L3 R2 + // L4/R1 + if( tolZero(t) ) + { + // t is zero -> part2 is input curve, part1 is empty (input.p0, that is) + part1.p0.x = part1.p1.x = part1.p2.x = part1.p3.x = input.p0.x; + part1.p0.y = part1.p1.y = part1.p2.y = part1.p3.y = input.p0.y; + part2 = input; + } + else if( tolEqual(t, 1.0) ) + { + // t is one -> part1 is input curve, part2 is empty (input.p3, that is) + part1 = input; + part2.p0.x = part2.p1.x = part2.p2.x = part2.p3.x = input.p3.x; + part2.p0.y = part2.p1.y = part2.p2.y = part2.p3.y = input.p3.y; + } + else + { + part1.p0.x = input.p0.x; part1.p0.y = input.p0.y; + part1.p1.x = (1.0 - t)*part1.p0.x + t*input.p1.x; part1.p1.y = (1.0 - t)*part1.p0.y + t*input.p1.y; + const double Hx ( (1.0 - t)*input.p1.x + t*input.p2.x ), Hy ( (1.0 - t)*input.p1.y + t*input.p2.y ); + part1.p2.x = (1.0 - t)*part1.p1.x + t*Hx; part1.p2.y = (1.0 - t)*part1.p1.y + t*Hy; + part2.p3.x = input.p3.x; part2.p3.y = input.p3.y; + part2.p2.x = (1.0 - t)*input.p2.x + t*input.p3.x; part2.p2.y = (1.0 - t)*input.p2.y + t*input.p3.y; + part2.p1.x = (1.0 - t)*Hx + t*part2.p2.x; part2.p1.y = (1.0 - t)*Hy + t*part2.p2.y; + part2.p0.x = (1.0 - t)*part1.p2.x + t*part2.p1.x; part2.p0.y = (1.0 - t)*part1.p2.y + t*part2.p1.y; + part1.p3.x = part2.p0.x; part1.p3.y = part2.p0.y; + } +} + +void printCurvesWithSafeRange( const Bezier& c1, const Bezier& c2, double t1_c1, double t2_c1, + const Bezier& c2_part, const FatLine& bounds_c2 ) +{ + static int offset = 0; + + std::cout << "# safe param range testing" << std::endl + << "plot [t=0.0:1.0] "; + + // clip safe ranges off c1 + Bezier c1_part1; + Bezier c1_part2; + Bezier c1_part3; + + // subdivide at t1_c1 + Impl_deCasteljauAt( c1_part1, c1_part2, c1, t1_c1 ); + // subdivide at t2_c1 + Impl_deCasteljauAt( c1_part1, c1_part3, c1_part2, t2_c1 ); + + // output remaining segment (c1_part1) + + std::cout << "bez(" + << c1.p0.x+offset << "," + << c1.p1.x+offset << "," + << c1.p2.x+offset << "," + << c1.p3.x+offset << ",t),bez(" + << c1.p0.y << "," + << c1.p1.y << "," + << c1.p2.y << "," + << c1.p3.y << ",t), bez(" + << c2.p0.x+offset << "," + << c2.p1.x+offset << "," + << c2.p2.x+offset << "," + << c2.p3.x+offset << ",t),bez(" + << c2.p0.y << "," + << c2.p1.y << "," + << c2.p2.y << "," + << c2.p3.y << ",t), " +#if 1 + << "bez(" + << c1_part1.p0.x+offset << "," + << c1_part1.p1.x+offset << "," + << c1_part1.p2.x+offset << "," + << c1_part1.p3.x+offset << ",t),bez(" + << c1_part1.p0.y << "," + << c1_part1.p1.y << "," + << c1_part1.p2.y << "," + << c1_part1.p3.y << ",t), " +#endif +#if 1 + << "bez(" + << c2_part.p0.x+offset << "," + << c2_part.p1.x+offset << "," + << c2_part.p2.x+offset << "," + << c2_part.p3.x+offset << ",t),bez(" + << c2_part.p0.y << "," + << c2_part.p1.y << "," + << c2_part.p2.y << "," + << c2_part.p3.y << ",t), " +#endif + << "linex(" + << bounds_c2.a << "," + << bounds_c2.b << "," + << bounds_c2.c << ",t)+" << offset << ", liney(" + << bounds_c2.a << "," + << bounds_c2.b << "," + << bounds_c2.c << ",t) title \"fat line (center)\", linex(" + << bounds_c2.a << "," + << bounds_c2.b << "," + << bounds_c2.c-bounds_c2.dMin << ",t)+" << offset << ", liney(" + << bounds_c2.a << "," + << bounds_c2.b << "," + << bounds_c2.c-bounds_c2.dMin << ",t) title \"fat line (min) \", linex(" + << bounds_c2.a << "," + << bounds_c2.b << "," + << bounds_c2.c-bounds_c2.dMax << ",t)+" << offset << ", liney(" + << bounds_c2.a << "," + << bounds_c2.b << "," + << bounds_c2.c-bounds_c2.dMax << ",t) title \"fat line (max) \"" << std::endl; + + offset += 1; +} + +void printResultWithFinalCurves( const Bezier& c1, const Bezier& c1_part, + const Bezier& c2, const Bezier& c2_part, + double t1_c1, double t2_c1 ) +{ + static int offset = 0; + + std::cout << "# final result" << std::endl + << "plot [t=0.0:1.0] "; + + std::cout << "bez(" + << c1.p0.x+offset << "," + << c1.p1.x+offset << "," + << c1.p2.x+offset << "," + << c1.p3.x+offset << ",t),bez(" + << c1.p0.y << "," + << c1.p1.y << "," + << c1.p2.y << "," + << c1.p3.y << ",t), bez(" + << c1_part.p0.x+offset << "," + << c1_part.p1.x+offset << "," + << c1_part.p2.x+offset << "," + << c1_part.p3.x+offset << ",t),bez(" + << c1_part.p0.y << "," + << c1_part.p1.y << "," + << c1_part.p2.y << "," + << c1_part.p3.y << ",t), " + << " pointmarkx(bez(" + << c1.p0.x+offset << "," + << c1.p1.x+offset << "," + << c1.p2.x+offset << "," + << c1.p3.x+offset << "," + << t1_c1 << "),t), " + << " pointmarky(bez(" + << c1.p0.y << "," + << c1.p1.y << "," + << c1.p2.y << "," + << c1.p3.y << "," + << t1_c1 << "),t), " + << " pointmarkx(bez(" + << c1.p0.x+offset << "," + << c1.p1.x+offset << "," + << c1.p2.x+offset << "," + << c1.p3.x+offset << "," + << t2_c1 << "),t), " + << " pointmarky(bez(" + << c1.p0.y << "," + << c1.p1.y << "," + << c1.p2.y << "," + << c1.p3.y << "," + << t2_c1 << "),t), " + + << "bez(" + << c2.p0.x+offset << "," + << c2.p1.x+offset << "," + << c2.p2.x+offset << "," + << c2.p3.x+offset << ",t),bez(" + << c2.p0.y << "," + << c2.p1.y << "," + << c2.p2.y << "," + << c2.p3.y << ",t), " + << "bez(" + << c2_part.p0.x+offset << "," + << c2_part.p1.x+offset << "," + << c2_part.p2.x+offset << "," + << c2_part.p3.x+offset << ",t),bez(" + << c2_part.p0.y << "," + << c2_part.p1.y << "," + << c2_part.p2.y << "," + << c2_part.p3.y << ",t)" << std::endl; + + offset += 1; +} + +/** determine parameter ranges [0,t1) and (t2,1] on c1, where c1 is guaranteed to lie outside c2. + Returns false, if the two curves don't even intersect. + + @param t1 + Range [0,t1) on c1 is guaranteed to lie outside c2 + + @param t2 + Range (t2,1] on c1 is guaranteed to lie outside c2 + + @param c1_orig + Original curve c1 + + @param c1_part + Subdivided current part of c1 + + @param c2_orig + Original curve c2 + + @param c2_part + Subdivided current part of c2 + */ +bool Impl_calcClipRange( double& t1, + double& t2, + const Bezier& c1_orig, + const Bezier& c1_part, + const Bezier& c2_orig, + const Bezier& c2_part ) +{ + // TODO: Maybe also check fat line orthogonal to P0P3, having P0 + // and P3 as the extremal points + + if( Impl_doBBoxIntersect(c1_part, c2_part) ) + { + // Calculate fat lines around c1 + FatLine bounds_c2; + + // must use the subdivided version of c2, since the fat line + // algorithm works implicitly with the convex hull bounding + // box. + Impl_calcFatLine(bounds_c2, c2_part); + + // determine clip positions on c2. Can use original c1 (which + // is necessary anyway, to get the t's on the original curve), + // since the distance calculations work directly in the + // Bernstein polynomial parameter domain. + if( Impl_calcSafeParams_clip( t1, t2, bounds_c2, + calcLineDistance( bounds_c2.a, + bounds_c2.b, + bounds_c2.c, + c1_orig.p0.x, + c1_orig.p0.y ), + calcLineDistance( bounds_c2.a, + bounds_c2.b, + bounds_c2.c, + c1_orig.p1.x, + c1_orig.p1.y ), + calcLineDistance( bounds_c2.a, + bounds_c2.b, + bounds_c2.c, + c1_orig.p2.x, + c1_orig.p2.y ), + calcLineDistance( bounds_c2.a, + bounds_c2.b, + bounds_c2.c, + c1_orig.p3.x, + c1_orig.p3.y ) ) ) + { + //printCurvesWithSafeRange(c1_orig, c2_orig, t1, t2, c2_part, bounds_c2); + + // they do intersect + return true; + } + } + + // they don't intersect: nothing to do + return false; +} + +/* Tangent intersection part + * ========================= + */ + +void Impl_calcFocus( Bezier& res, const Bezier& c ) +{ + // arbitrary small value, for now + // TODO: find meaningful value + const double minPivotValue( 1.0e-20 ); + + Point2D::value_type fMatrix[6]; + Point2D::value_type fRes[2]; + + // calc new curve from hodograph, c and linear blend + + // Coefficients for derivative of c are (C_i=n(C_{i+1} - C_i)): + + // 3(P1 - P0), 3(P2 - P1), 3(P3 - P2) (bezier curve of degree 2) + + // The hodograph is then (bezier curve of 2nd degree is P0(1-t)^2 + 2P1(1-t)t + P2t^2): + + // 3(P1 - P0)(1-t)^2 + 6(P2 - P1)(1-t)t + 3(P3 - P2)t^2 + + // rotate by 90 degrees: x=-y, y=x and you get the normal vector function N(t): + + // x(t) = -(3(P1.y - P0.y)(1-t)^2 + 6(P2.y - P1.y)(1-t)t + 3(P3.y - P2.y)t^2) + // y(t) = 3(P1.x - P0.x)(1-t)^2 + 6(P2.x - P1.x)(1-t)t + 3(P3.x - P2.x)t^2 + + // Now, the focus curve is defined to be F(t)=P(t) + c(t)N(t), + // where P(t) is the original curve, and c(t)=c0(1-t) + c1 t + + // This results in the following expression for F(t): + + // x(t) = P0.x (1-t)^3 + 3 P1.x (1-t)^2t + 3 P2.x (1.t)t^2 + P3.x t^3 - + // (c0(1-t) + c1 t)(3(P1.y - P0.y)(1-t)^2 + 6(P2.y - P1.y)(1-t)t + 3(P3.y - P2.y)t^2) + + // y(t) = P0.y (1-t)^3 + 3 P1.y (1-t)^2t + 3 P2.y (1.t)t^2 + P3.y t^3 + + // (c0(1-t) + c1 t)(3(P1.x - P0.x)(1-t)^2 + 6(P2.x - P1.x)(1-t)t + 3(P3.x - P2.x)t^2) + + // As a heuristic, we set F(0)=F(1) (thus, the curve is closed and _tends_ to be small): + + // For F(0), the following results: + + // x(0) = P0.x - c0 3(P1.y - P0.y) + // y(0) = P0.y + c0 3(P1.x - P0.x) + + // For F(1), the following results: + + // x(1) = P3.x - c1 3(P3.y - P2.y) + // y(1) = P3.y + c1 3(P3.x - P2.x) + + // Reorder, collect and substitute into F(0)=F(1): + + // P0.x - c0 3(P1.y - P0.y) = P3.x - c1 3(P3.y - P2.y) + // P0.y + c0 3(P1.x - P0.x) = P3.y + c1 3(P3.x - P2.x) + + // which yields + + // (P0.y - P1.y)c0 + (P3.y - P2.y)c1 = (P3.x - P0.x)/3 + // (P1.x - P0.x)c0 + (P2.x - P3.x)c1 = (P3.y - P0.y)/3 + + // so, this is what we calculate here (determine c0 and c1): + fMatrix[0] = c.p1.x - c.p0.x; + fMatrix[1] = c.p2.x - c.p3.x; + fMatrix[2] = (c.p3.y - c.p0.y)/3.0; + fMatrix[3] = c.p0.y - c.p1.y; + fMatrix[4] = c.p3.y - c.p2.y; + fMatrix[5] = (c.p3.x - c.p0.x)/3.0; + + // TODO: determine meaningful value for + if( !solve(fMatrix, 2, 3, fRes, minPivotValue) ) + { + // TODO: generate meaningful values here + // singular or nearly singular system -- use arbitrary + // values for res + fRes[0] = 0.0; + fRes[1] = 1.0; + + std::cerr << "Matrix singular!" << std::endl; + } + + // now, the reordered and per-coefficient collected focus curve is + // the following third degree bezier curve F(t): + + // x(t) = P0.x (1-t)^3 + 3 P1.x (1-t)^2t + 3 P2.x (1.t)t^2 + P3.x t^3 - + // (c0(1-t) + c1 t)(3(P1.y - P0.y)(1-t)^2 + 6(P2.y - P1.y)(1-t)t + 3(P3.y - P2.y)t^2) + // = P0.x (1-t)^3 + 3 P1.x (1-t)^2t + 3 P2.x (1.t)t^2 + P3.x t^3 - + // (3c0P1.y(1-t)^3 - 3c0P0.y(1-t)^3 + 6c0P2.y(1-t)^2t - 6c0P1.y(1-t)^2t + + // 3c0P3.y(1-t)t^2 - 3c0P2.y(1-t)t^2 + + // 3c1P1.y(1-t)^2t - 3c1P0.y(1-t)^2t + 6c1P2.y(1-t)t^2 - 6c1P1.y(1-t)t^2 + + // 3c1P3.yt^3 - 3c1P2.yt^3) + // = (P0.x - 3 c0 P1.y + 3 c0 P0.y)(1-t)^3 + + // 3(P1.x - c1 P1.y + c1 P0.y - 2 c0 P2.y + 2 c0 P1.y)(1-t)^2t + + // 3(P2.x - 2 c1 P2.y + 2 c1 P1.y - c0 P3.y + c0 P2.y)(1-t)t^2 + + // (P3.x - 3 c1 P3.y + 3 c1 P2.y)t^3 + // = (P0.x - 3 c0(P1.y - P0.y))(1-t)^3 + + // 3(P1.x - c1(P1.y - P0.y) - 2c0(P2.y - P1.y))(1-t)^2t + + // 3(P2.x - 2 c1(P2.y - P1.y) - c0(P3.y - P2.y))(1-t)t^2 + + // (P3.x - 3 c1(P3.y - P2.y))t^3 + + // y(t) = P0.y (1-t)^3 + 3 P1.y (1-t)^2t + 3 P2.y (1-t)t^2 + P3.y t^3 + + // (c0(1-t) + c1 t)(3(P1.x - P0.x)(1-t)^2 + 6(P2.x - P1.x)(1-t)t + 3(P3.x - P2.x)t^2) + // = P0.y (1-t)^3 + 3 P1.y (1-t)^2t + 3 P2.y (1-t)t^2 + P3.y t^3 + + // 3c0(P1.x - P0.x)(1-t)^3 + 6c0(P2.x - P1.x)(1-t)^2t + 3c0(P3.x - P2.x)(1-t)t^2 + + // 3c1(P1.x - P0.x)(1-t)^2t + 6c1(P2.x - P1.x)(1-t)t^2 + 3c1(P3.x - P2.x)t^3 + // = (P0.y + 3 c0 (P1.x - P0.x))(1-t)^3 + + // 3(P1.y + 2 c0 (P2.x - P1.x) + c1 (P1.x - P0.x))(1-t)^2t + + // 3(P2.y + c0 (P3.x - P2.x) + 2 c1 (P2.x - P1.x))(1-t)t^2 + + // (P3.y + 3 c1 (P3.x - P2.x))t^3 + + // Therefore, the coefficients F0 to F3 of the focus curve are: + + // F0.x = (P0.x - 3 c0(P1.y - P0.y)) F0.y = (P0.y + 3 c0 (P1.x - P0.x)) + // F1.x = (P1.x - c1(P1.y - P0.y) - 2c0(P2.y - P1.y)) F1.y = (P1.y + 2 c0 (P2.x - P1.x) + c1 (P1.x - P0.x)) + // F2.x = (P2.x - 2 c1(P2.y - P1.y) - c0(P3.y - P2.y)) F2.y = (P2.y + c0 (P3.x - P2.x) + 2 c1 (P2.x - P1.x)) + // F3.x = (P3.x - 3 c1(P3.y - P2.y)) F3.y = (P3.y + 3 c1 (P3.x - P2.x)) + + res.p0.x = c.p0.x - 3*fRes[0]*(c.p1.y - c.p0.y); + res.p1.x = c.p1.x - fRes[1]*(c.p1.y - c.p0.y) - 2*fRes[0]*(c.p2.y - c.p1.y); + res.p2.x = c.p2.x - 2*fRes[1]*(c.p2.y - c.p1.y) - fRes[0]*(c.p3.y - c.p2.y); + res.p3.x = c.p3.x - 3*fRes[1]*(c.p3.y - c.p2.y); + + res.p0.y = c.p0.y + 3*fRes[0]*(c.p1.x - c.p0.x); + res.p1.y = c.p1.y + 2*fRes[0]*(c.p2.x - c.p1.x) + fRes[1]*(c.p1.x - c.p0.x); + res.p2.y = c.p2.y + fRes[0]*(c.p3.x - c.p2.x) + 2*fRes[1]*(c.p2.x - c.p1.x); + res.p3.y = c.p3.y + 3*fRes[1]*(c.p3.x - c.p2.x); +} + +bool Impl_calcSafeParams_focus( double& t1, + double& t2, + const Bezier& curve, + const Bezier& focus ) +{ + // now, we want to determine which normals of the original curve + // P(t) intersect with the focus curve F(t). The condition for + // this statement is P'(t)(P(t) - F) = 0, i.e. hodograph P'(t) and + // line through P(t) and F are perpendicular. + // If you expand this equation, you end up with something like + + // (\sum_{i=0}^n (P_i - F)B_i^n(t))^T (\sum_{j=0}^{n-1} n(P_{j+1} - P_j)B_j^{n-1}(t)) + + // Multiplying that out (as the scalar product is linear, we can + // extract some terms) yields: + + // (P_i - F)^T n(P_{j+1} - P_j) B_i^n(t)B_j^{n-1}(t) + ... + + // If we combine the B_i^n(t)B_j^{n-1}(t) product, we arrive at a + // Bernstein polynomial of degree 2n-1, as + + // \binom{n}{i}(1-t)^{n-i}t^i) \binom{n-1}{j}(1-t)^{n-1-j}t^j) = + // \binom{n}{i}\binom{n-1}{j}(1-t)^{2n-1-i-j}t^{i+j} + + // Thus, with the defining equation for a 2n-1 degree Bernstein + // polynomial + + // \sum_{i=0}^{2n-1} d_i B_i^{2n-1}(t) + + // the d_i are calculated as follows: + + // d_i = \sum_{j+k=i, j\in\{0,...,n\}, k\in\{0,...,n-1\}} \frac{\binom{n}{j}\binom{n-1}{k}}{\binom{2n-1}{i}} n (P_{k+1} - P_k)^T(P_j - F) + + // Okay, but F is now not a single point, but itself a curve + // F(u). Thus, for every value of u, we get a different 2n-1 + // bezier curve from the above equation. Therefore, we have a + // tensor product bezier patch, with the following defining + // equation: + + // d(t,u) = \sum_{i=0}^{2n-1} \sum_{j=0}^m B_i^{2n-1}(t) B_j^{m}(u) d_{ij}, where + // d_{ij} = \sum_{k+l=i, l\in\{0,...,n\}, k\in\{0,...,n-1\}} \frac{\binom{n}{l}\binom{n-1}{k}}{\binom{2n-1}{i}} n (P_{k+1} - P_k)^T(P_l - F_j) + + // as above, only that now F is one of the focus' control points. + + // Note the difference in the binomial coefficients to the + // reference paper, these formulas most probably contained a typo. + + // To determine, where D(t,u) is _not_ zero (these are the parts + // of the curve that don't share normals with the focus and can + // thus be safely clipped away), we project D(u,t) onto the + // (d(t,u), t) plane, determine the convex hull there and proceed + // as for the curve intersection part (projection is orthogonal to + // u axis, thus simply throw away u coordinate). + + // \fallfac are so-called falling factorials (see Concrete + // Mathematics, p. 47 for a definition). + + // now, for tensor product bezier curves, the convex hull property + // holds, too. Thus, we simply project the control points (t_{ij}, + // u_{ij}, d_{ij}) onto the (t,d) plane and calculate the + // intersections of the convex hull with the t axis, as for the + // bezier clipping case. + + // calc polygon of control points (t_{ij}, d_{ij}): + + const int n( 3 ); // cubic bezier curves, as a matter of fact + const int i_card( 2*n ); + const int j_card( n + 1 ); + const int k_max( n-1 ); + Polygon2D controlPolygon( i_card*j_card ); // vector of (t_{ij}, d_{ij}) in row-major order + + int i, j, k, l; // variable notation from formulas above and Sederberg article + Point2D::value_type d; + for( i=0; i<i_card; ++i ) + { + for( j=0; j<j_card; ++j ) + { + // calc single d_{ij} sum: + for( d=0.0, k=std::max(0,i-n); k<=k_max && k<=i; ++k ) + { + l = i - k; // invariant: k + l = i + assert(k>=0 && k<=n-1); // k \in {0,...,n-1} + assert(l>=0 && l<=n); // l \in {0,...,n} + + // TODO: find, document and assert proper limits for n and int's max_val. + // This becomes important should anybody wants to use + // this code for higher-than-cubic beziers + d += static_cast<double>(fallFac(n,l)*fallFac(n-1,k)*fac(i)) / + static_cast<double>(fac(l)*fac(k) * fallFac(2*n-1,i)) * n * + ( (curve[k+1].x - curve[k].x)*(curve[l].x - focus[j].x) + // dot product here + (curve[k+1].y - curve[k].y)*(curve[l].y - focus[j].y) ); + } + + // Note that the t_{ij} values are evenly spaced on the + // [0,1] interval, thus t_{ij}=i/(2n-1) + controlPolygon[ i*j_card + j ] = Point2D( i/(2.0*n-1.0), d ); + } + } + +#ifndef WITH_SAFEFOCUSPARAM_DETAILED_TEST + + // calc safe parameter range, to determine [0,t1] and [t2,1] where + // no zero crossing is guaranteed. + return Impl_calcSafeParams( t1, t2, controlPolygon, 0.0, 0.0 ); + +#else + bool bRet( Impl_calcSafeParams( t1, t2, controlPolygon, 0.0, 0.0 ) ); + + Polygon2D convHull( convexHull( controlPolygon ) ); + + std::cout << "# convex hull testing (focus)" << std::endl + << "plot [t=0:1] "; + std::cout << "'-' using ($1):($2) title \"control polygon\" with lp, " + << "'-' using ($1):($2) title \"convex hull\" with lp" << std::endl; + + unsigned int count; + for( count=0; count<controlPolygon.size(); ++count ) + { + std::cout << controlPolygon[count].x << " " << controlPolygon[count].y << std::endl; + } + std::cout << controlPolygon[0].x << " " << controlPolygon[0].y << std::endl; + std::cout << "e" << std::endl; + + for( count=0; count<convHull.size(); ++count ) + { + std::cout << convHull[count].x << " " << convHull[count].y << std::endl; + } + std::cout << convHull[0].x << " " << convHull[0].y << std::endl; + std::cout << "e" << std::endl; + + return bRet; +#endif +} + +/** Calc all values t_i on c1, for which safeRanges functor does not + give a safe range on c1 and c2. + + This method is the workhorse of the bezier clipping. Because c1 + and c2 must be alternatingly tested against each other (first + determine safe parameter interval on c1 with regard to c2, then + the other way around), we call this method recursively with c1 and + c2 swapped. + + @param result + Output iterator where the final t values are added to. If curves + don't intersect, nothing is added. + + @param delta + Maximal allowed distance to true critical point (measured in the + original curve's coordinate system) + + @param safeRangeFunctor + Functor object, that must provide the following operator(): + bool safeRangeFunctor( double& t1, + double& t2, + const Bezier& c1_orig, + const Bezier& c1_part, + const Bezier& c2_orig, + const Bezier& c2_part ); + This functor must calculate the safe ranges [0,t1] and [t2,1] on + c1_orig, where c1_orig is 'safe' from c2_part. If the whole + c1_orig is safe, false must be returned, true otherwise. + */ +template <class Functor> void Impl_applySafeRanges_rec( std::back_insert_iterator< std::vector< std::pair<double, double> > >& result, + double delta, + const Functor& safeRangeFunctor, + int recursionLevel, + const Bezier& c1_orig, + const Bezier& c1_part, + double last_t1_c1, + double last_t2_c1, + const Bezier& c2_orig, + const Bezier& c2_part, + double last_t1_c2, + double last_t2_c2 ) +{ + // check end condition + // =================== + + // TODO: tidy up recursion handling. maybe put everything in a + // struct and swap that here at method entry + + // TODO: Implement limit on recursion depth. Should that limit be + // reached, chances are that we're on a higher-order tangency. For + // this case, AW proposed to take the middle of the current + // interval, and to correct both curve's tangents at that new + // endpoint to be equal. That virtually generates a first-order + // tangency, and justifies to return a single intersection + // point. Otherwise, inside/outside test might fail here. + + for( int i=0; i<recursionLevel; ++i ) std::cerr << " "; + if( recursionLevel % 2 ) + { + std::cerr << std::endl << "level: " << recursionLevel + << " t: " + << last_t1_c2 + (last_t2_c2 - last_t1_c2)/2.0 + << ", c1: " << last_t1_c2 << " " << last_t2_c2 + << ", c2: " << last_t1_c1 << " " << last_t2_c1 + << std::endl; + } + else + { + std::cerr << std::endl << "level: " << recursionLevel + << " t: " + << last_t1_c1 + (last_t2_c1 - last_t1_c1)/2.0 + << ", c1: " << last_t1_c1 << " " << last_t2_c1 + << ", c2: " << last_t1_c2 << " " << last_t2_c2 + << std::endl; + } + + // refine solution + // =============== + + double t1_c1, t2_c1; + + // Note: we first perform the clipping and only test for precision + // sufficiency afterwards, since we want to exploit the fact that + // Impl_calcClipRange returns false if the curves don't + // intersect. We would have to check that separately for the end + // condition, otherwise. + + // determine safe range on c1_orig + if( safeRangeFunctor( t1_c1, t2_c1, c1_orig, c1_part, c2_orig, c2_part ) ) + { + // now, t1 and t2 are calculated on the original curve + // (but against a fat line calculated from the subdivided + // c2, namely c2_part). If the [t1,t2] range is outside + // our current [last_t1,last_t2] range, we're done in this + // branch - the curves no longer intersect. + if( tolLessEqual(t1_c1, last_t2_c1) && tolGreaterEqual(t2_c1, last_t1_c1) ) + { + // As noted above, t1 and t2 are calculated on the + // original curve, but against a fat line + // calculated from the subdivided c2, namely + // c2_part. Our domain to work on is + // [last_t1,last_t2], on the other hand, so values + // of [t1,t2] outside that range are irrelevant + // here. Clip range appropriately. + t1_c1 = std::max(t1_c1, last_t1_c1); + t2_c1 = std::min(t2_c1, last_t2_c1); + + // TODO: respect delta + // for now, end condition is just a fixed threshold on the t's + + // check end condition + // =================== + +#if 1 + if( fabs(last_t2_c1 - last_t1_c1) < 0.0001 && + fabs(last_t2_c2 - last_t1_c2) < 0.0001 ) +#else + if( fabs(last_t2_c1 - last_t1_c1) < 0.01 && + fabs(last_t2_c2 - last_t1_c2) < 0.01 ) +#endif + { + // done. Add to result + if( recursionLevel % 2 ) + { + // uneven level: have to swap the t's, since curves are swapped, too + *result++ = std::make_pair( last_t1_c2 + (last_t2_c2 - last_t1_c2)/2.0, + last_t1_c1 + (last_t2_c1 - last_t1_c1)/2.0 ); + } + else + { + *result++ = std::make_pair( last_t1_c1 + (last_t2_c1 - last_t1_c1)/2.0, + last_t1_c2 + (last_t2_c2 - last_t1_c2)/2.0 ); + } + +#if 0 + //printResultWithFinalCurves( c1_orig, c1_part, c2_orig, c2_part, last_t1_c1, last_t2_c1 ); + printResultWithFinalCurves( c1_orig, c1_part, c2_orig, c2_part, t1_c1, t2_c1 ); +#else + // calc focus curve of c2 + Bezier focus; + Impl_calcFocus(focus, c2_part); // need to use subdivided c2 + + safeRangeFunctor( t1_c1, t2_c1, c1_orig, c1_part, c2_orig, c2_part ); + + //printResultWithFinalCurves( c1_orig, c1_part, c2_orig, focus, t1_c1, t2_c1 ); + printResultWithFinalCurves( c1_orig, c1_part, c2_orig, focus, last_t1_c1, last_t2_c1 ); +#endif + } + else + { + // heuristic: if parameter range is not reduced by at least + // 20%, subdivide longest curve, and clip shortest against + // both parts of longest +// if( (last_t2_c1 - last_t1_c1 - t2_c1 + t1_c1) / (last_t2_c1 - last_t1_c1) < 0.2 ) + if( false ) + { + // subdivide and descend + // ===================== + + Bezier part1; + Bezier part2; + + double intervalMiddle; + + if( last_t2_c1 - last_t1_c1 > last_t2_c2 - last_t1_c2 ) + { + // subdivide c1 + // ============ + + intervalMiddle = last_t1_c1 + (last_t2_c1 - last_t1_c1)/2.0; + + // subdivide at the middle of the interval (as + // we're not subdividing on the original + // curve, this simply amounts to subdivision + // at 0.5) + Impl_deCasteljauAt( part1, part2, c1_part, 0.5 ); + + // and descend recursively with swapped curves + Impl_applySafeRanges_rec( result, delta, safeRangeFunctor, recursionLevel+1, + c2_orig, c2_part, last_t1_c2, last_t2_c2, + c1_orig, part1, last_t1_c1, intervalMiddle ); + + Impl_applySafeRanges_rec( result, delta, safeRangeFunctor, recursionLevel+1, + c2_orig, c2_part, last_t1_c2, last_t2_c2, + c1_orig, part2, intervalMiddle, last_t2_c1 ); + } + else + { + // subdivide c2 + // ============ + + intervalMiddle = last_t1_c2 + (last_t2_c2 - last_t1_c2)/2.0; + + // subdivide at the middle of the interval (as + // we're not subdividing on the original + // curve, this simply amounts to subdivision + // at 0.5) + Impl_deCasteljauAt( part1, part2, c2_part, 0.5 ); + + // and descend recursively with swapped curves + Impl_applySafeRanges_rec( result, delta, safeRangeFunctor, recursionLevel+1, + c2_orig, part1, last_t1_c2, intervalMiddle, + c1_orig, c1_part, last_t1_c1, last_t2_c1 ); + + Impl_applySafeRanges_rec( result, delta, safeRangeFunctor, recursionLevel+1, + c2_orig, part2, intervalMiddle, last_t2_c2, + c1_orig, c1_part, last_t1_c1, last_t2_c1 ); + } + } + else + { + // apply calculated clip + // ===================== + + // clip safe ranges off c1_orig + Bezier c1_part1; + Bezier c1_part2; + Bezier c1_part3; + + // subdivide at t1_c1 + Impl_deCasteljauAt( c1_part1, c1_part2, c1_orig, t1_c1 ); + + // subdivide at t2_c1. As we're working on + // c1_part2 now, we have to adapt t2_c1 since + // we're no longer in the original parameter + // interval. This is based on the following + // assumption: t2_new = (t2-t1)/(1-t1), which + // relates the t2 value into the new parameter + // range [0,1] of c1_part2. + Impl_deCasteljauAt( c1_part1, c1_part3, c1_part2, (t2_c1-t1_c1)/(1.0-t1_c1) ); + + // descend with swapped curves and c1_part1 as the + // remaining (middle) segment + Impl_applySafeRanges_rec( result, delta, safeRangeFunctor, recursionLevel+1, + c2_orig, c2_part, last_t1_c2, last_t2_c2, + c1_orig, c1_part1, t1_c1, t2_c1 ); + } + } + } + } +} + +struct ClipBezierFunctor +{ + bool operator()( double& t1_c1, + double& t2_c1, + const Bezier& c1_orig, + const Bezier& c1_part, + const Bezier& c2_orig, + const Bezier& c2_part ) const + { + return Impl_calcClipRange( t1_c1, t2_c1, c1_orig, c1_part, c2_orig, c2_part ); + } +}; + +struct BezierTangencyFunctor +{ + bool operator()( double& t1_c1, + double& t2_c1, + const Bezier& c1_orig, + const Bezier& c1_part, + const Bezier& c2_orig, + const Bezier& c2_part ) const + { + // calc focus curve of c2 + Bezier focus; + Impl_calcFocus(focus, c2_part); // need to use subdivided c2 + // here, as the whole curve is + // used for focus calculation + + // determine safe range on c1_orig + bool bRet( Impl_calcSafeParams_focus( t1_c1, t2_c1, + c1_orig, // use orig curve here, need t's on original curve + focus ) ); + + std::cerr << "range: " << t2_c1 - t1_c1 << ", ret: " << bRet << std::endl; + + return bRet; + } +}; + +/** Perform a bezier clip (curve against curve) + + @param result + Output iterator where the final t values are added to. This + iterator will remain empty, if there are no intersections. + + @param delta + Maximal allowed distance to true intersection (measured in the + original curve's coordinate system) + */ +void clipBezier( std::back_insert_iterator< std::vector< std::pair<double, double> > >& result, + double delta, + const Bezier& c1, + const Bezier& c2 ) +{ +#if 0 + // first of all, determine list of collinear normals. Collinear + // normals typically separate two intersections, thus, subdivide + // at all collinear normal's t values beforehand. This will cater + // for tangent intersections, where two or more intersections are + // infinitesimally close together. + + // TODO: evaluate effects of higher-than-second-order + // tangencies. Sederberg et al. state that collinear normal + // algorithm then degrades quickly. + + std::vector< std::pair<double,double> > results; + std::back_insert_iterator< std::vector< std::pair<double, double> > > ii(results); + + Impl_calcCollinearNormals( ii, delta, 0, c1, c1, 0.0, 1.0, c2, c2, 0.0, 1.0 ); + + // As Sederberg's collinear normal theorem is only sufficient, not + // necessary for two intersections left and right, we've to test + // all segments generated by the collinear normal algorithm + // against each other. In other words, if the two curves are both + // divided in a left and a right part, the collinear normal + // theorem does _not_ state that the left part of curve 1 does not + // e.g. intersect with the right part of curve 2. + + // divide c1 and c2 at collinear normal intersection points + std::vector< Bezier > c1_segments( results.size()+1 ); + std::vector< Bezier > c2_segments( results.size()+1 ); + Bezier c1_remainder( c1 ); + Bezier c2_remainder( c2 ); + unsigned int i; + for( i=0; i<results.size(); ++i ) + { + Bezier c1_part2; + Impl_deCasteljauAt( c1_segments[i], c1_part2, c1_remainder, results[i].first ); + c1_remainder = c1_part2; + + Bezier c2_part2; + Impl_deCasteljauAt( c2_segments[i], c2_part2, c2_remainder, results[i].second ); + c2_remainder = c2_part2; + } + c1_segments[i] = c1_remainder; + c2_segments[i] = c2_remainder; + + // now, c1/c2_segments contain all segments, then + // clip every resulting segment against every other + unsigned int c1_curr, c2_curr; + for( c1_curr=0; c1_curr<c1_segments.size(); ++c1_curr ) + { + for( c2_curr=0; c2_curr<c2_segments.size(); ++c2_curr ) + { + if( c1_curr != c2_curr ) + { + Impl_clipBezier_rec(result, delta, 0, + c1_segments[c1_curr], c1_segments[c1_curr], + 0.0, 1.0, + c2_segments[c2_curr], c2_segments[c2_curr], + 0.0, 1.0); + } + } + } +#else + Impl_applySafeRanges_rec( result, delta, BezierTangencyFunctor(), 0, c1, c1, 0.0, 1.0, c2, c2, 0.0, 1.0 ); + //Impl_applySafeRanges_rec( result, delta, ClipBezierFunctor(), 0, c1, c1, 0.0, 1.0, c2, c2, 0.0, 1.0 ); +#endif + // that's it, boys'n'girls! +} + +int main(int argc, const char *argv[]) +{ + double curr_Offset( 0 ); + unsigned int i,j,k; + + Bezier someCurves[] = + { +// {Point2D(0.0,0.0),Point2D(0.0,1.0),Point2D(1.0,1.0),Point2D(1.0,0.0)}, +// {Point2D(0.0,0.0),Point2D(0.0,1.0),Point2D(1.0,1.0),Point2D(1.0,0.5)}, +// {Point2D(1.0,0.0),Point2D(0.0,0.0),Point2D(0.0,1.0),Point2D(1.0,1.0)} +// {Point2D(0.25+1,0.5),Point2D(0.25+1,0.708333),Point2D(0.423611+1,0.916667),Point2D(0.770833+1,0.980324)}, +// {Point2D(0.0+1,0.0),Point2D(0.0+1,1.0),Point2D(1.0+1,1.0),Point2D(1.0+1,0.5)} + +// tangency1 +// {Point2D(0.627124+1,0.828427),Point2D(0.763048+1,0.828507),Point2D(0.885547+1,0.77312),Point2D(0.950692+1,0.67325)}, +// {Point2D(0.0,1.0),Point2D(0.1,1.0),Point2D(0.4,1.0),Point2D(0.5,1.0)} + +// {Point2D(0.0,0.0),Point2D(0.0,1.0),Point2D(1.0,1.0),Point2D(1.0,0.5)}, +// {Point2D(0.60114,0.933091),Point2D(0.69461,0.969419),Point2D(0.80676,0.992976),Point2D(0.93756,0.998663)} +// {Point2D(1.0,0.0),Point2D(0.0,0.0),Point2D(0.0,1.0),Point2D(1.0,1.0)}, +// {Point2D(0.62712,0.828427),Point2D(0.76305,0.828507),Point2D(0.88555,0.77312),Point2D(0.95069,0.67325)} + +// clipping1 +// {Point2D(0.0,0.0),Point2D(0.0,3.5),Point2D(1.0,-2.5),Point2D(1.0,1.0)}, +// {Point2D(0.0,1.0),Point2D(3.5,1.0),Point2D(-2.5,0.0),Point2D(1.0,0.0)} + +// tangency2 +// {Point2D(0.0,1.0),Point2D(3.5,1.0),Point2D(-2.5,0.0),Point2D(1.0,0.0)}, +// {Point2D(15.3621,0.00986464),Point2D(15.3683,0.0109389),Point2D(15.3682,0.0109315),Point2D(15.3621,0.00986464)} + +// tangency3 +// {Point2D(1.0,0.0),Point2D(0.0,0.0),Point2D(0.0,1.0),Point2D(1.0,1.0)}, +// {Point2D(-0.5,0.0),Point2D(0.5,0.0),Point2D(0.5,1.0),Point2D(-0.5,1.0)} + +// tangency4 +// {Point2D(-0.5,0.0),Point2D(0.5,0.0),Point2D(0.5,1.0),Point2D(-0.5,1.0)}, +// {Point2D(0.26,0.4),Point2D(0.25,0.5),Point2D(0.25,0.5),Point2D(0.26,0.6)} + +// tangency5 +// {Point2D(0.0,0.0),Point2D(0.0,3.5),Point2D(1.0,-2.5),Point2D(1.0,1.0)}, +// {Point2D(15.3621,0.00986464),Point2D(15.3683,0.0109389),Point2D(15.3682,0.0109315),Point2D(15.3621,0.00986464)} + +// tangency6 +// {Point2D(0.0,0.0),Point2D(0.0,3.5),Point2D(1.0,-2.5),Point2D(1.0,1.0)}, +// {Point2D(15.3621,10.00986464),Point2D(15.3683,10.0109389),Point2D(15.3682,10.0109315),Point2D(15.3621,10.00986464)} + +// tangency7 +// {Point2D(2.505,0.0),Point2D(2.505+4.915,4.300),Point2D(2.505+3.213,10.019),Point2D(2.505-2.505,10.255)}, +// {Point2D(15.3621,10.00986464),Point2D(15.3683,10.0109389),Point2D(15.3682,10.0109315),Point2D(15.3621,10.00986464)} + +// tangency Sederberg example + {Point2D(2.505,0.0),Point2D(2.505+4.915,4.300),Point2D(2.505+3.213,10.019),Point2D(2.505-2.505,10.255)}, + {Point2D(5.33+9.311,0.0),Point2D(5.33+9.311-13.279,4.205),Point2D(5.33+9.311-10.681,9.119),Point2D(5.33+9.311-2.603,10.254)} + +// clipping2 +// {Point2D(-0.5,0.0),Point2D(0.5,0.0),Point2D(0.5,1.0),Point2D(-0.5,1.0)}, +// {Point2D(0.2575,0.4),Point2D(0.2475,0.5),Point2D(0.2475,0.5),Point2D(0.2575,0.6)} + +// {Point2D(0.0,0.1),Point2D(0.2,3.5),Point2D(1.0,-2.5),Point2D(1.1,1.2)}, +// {Point2D(0.0,1.0),Point2D(3.5,0.9),Point2D(-2.5,0.1),Point2D(1.1,0.2)} +// {Point2D(0.0,0.1),Point2D(0.2,3.0),Point2D(1.0,-2.0),Point2D(1.1,1.2)}, +// {Point2D(0.627124+1,0.828427),Point2D(0.763048+1,0.828507),Point2D(0.885547+1,0.77312),Point2D(0.950692+1,0.67325)} +// {Point2D(0.0,1.0),Point2D(3.0,0.9),Point2D(-2.0,0.1),Point2D(1.1,0.2)} +// {Point2D(0.0,4.0),Point2D(0.1,5.0),Point2D(0.9,5.0),Point2D(1.0,4.0)}, +// {Point2D(0.0,0.0),Point2D(0.1,0.5),Point2D(0.9,0.5),Point2D(1.0,0.0)}, +// {Point2D(0.0,0.1),Point2D(0.1,1.5),Point2D(0.9,1.5),Point2D(1.0,0.1)}, +// {Point2D(0.0,-4.0),Point2D(0.1,-5.0),Point2D(0.9,-5.0),Point2D(1.0,-4.0)} + }; + + // output gnuplot setup + std::cout << "#!/usr/bin/gnuplot -persist" << std::endl + << "#" << std::endl + << "# automatically generated by bezierclip, don't change!" << std::endl + << "#" << std::endl + << "set parametric" << std::endl + << "bez(p,q,r,s,t) = p*(1-t)**3+q*3*(1-t)**2*t+r*3*(1-t)*t**2+s*t**3" << std::endl + << "bezd(p,q,r,s,t) = 3*(q-p)*(1-t)**2+6*(r-q)*(1-t)*t+3*(s-r)*t**2" << std::endl + << "pointmarkx(c,t) = c-0.03*t" << std::endl + << "pointmarky(c,t) = c+0.03*t" << std::endl + << "linex(a,b,c,t) = a*-c + t*-b" << std::endl + << "liney(a,b,c,t) = b*-c + t*a" << std::endl << std::endl + << "# end of setup" << std::endl << std::endl; + +#ifdef WITH_CONVEXHULL_TEST + // test convex hull algorithm + const double convHull_xOffset( curr_Offset ); + curr_Offset += 20; + std::cout << "# convex hull testing" << std::endl + << "plot [t=0:1] "; + for( i=0; i<sizeof(someCurves)/sizeof(Bezier); ++i ) + { + Polygon2D aTestPoly(4); + aTestPoly[0] = someCurves[i].p0; + aTestPoly[1] = someCurves[i].p1; + aTestPoly[2] = someCurves[i].p2; + aTestPoly[3] = someCurves[i].p3; + + aTestPoly[0].x += convHull_xOffset; + aTestPoly[1].x += convHull_xOffset; + aTestPoly[2].x += convHull_xOffset; + aTestPoly[3].x += convHull_xOffset; + + std::cout << " bez(" + << aTestPoly[0].x << "," + << aTestPoly[1].x << "," + << aTestPoly[2].x << "," + << aTestPoly[3].x << ",t),bez(" + << aTestPoly[0].y << "," + << aTestPoly[1].y << "," + << aTestPoly[2].y << "," + << aTestPoly[3].y << ",t), '-' using ($1):($2) title \"convex hull " << i << "\" with lp"; + + if( i+1<sizeof(someCurves)/sizeof(Bezier) ) + std::cout << ",\\" << std::endl; + else + std::cout << std::endl; + } + for( i=0; i<sizeof(someCurves)/sizeof(Bezier); ++i ) + { + Polygon2D aTestPoly(4); + aTestPoly[0] = someCurves[i].p0; + aTestPoly[1] = someCurves[i].p1; + aTestPoly[2] = someCurves[i].p2; + aTestPoly[3] = someCurves[i].p3; + + aTestPoly[0].x += convHull_xOffset; + aTestPoly[1].x += convHull_xOffset; + aTestPoly[2].x += convHull_xOffset; + aTestPoly[3].x += convHull_xOffset; + + Polygon2D convHull( convexHull(aTestPoly) ); + + for( k=0; k<convHull.size(); ++k ) + { + std::cout << convHull[k].x << " " << convHull[k].y << std::endl; + } + std::cout << convHull[0].x << " " << convHull[0].y << std::endl; + std::cout << "e" << std::endl; + } +#endif + +#ifdef WITH_MULTISUBDIVIDE_TEST + // test convex hull algorithm + const double multiSubdivide_xOffset( curr_Offset ); + curr_Offset += 20; + std::cout << "# multi subdivide testing" << std::endl + << "plot [t=0:1] "; + for( i=0; i<sizeof(someCurves)/sizeof(Bezier); ++i ) + { + Bezier c( someCurves[i] ); + Bezier c1_part1; + Bezier c1_part2; + Bezier c1_part3; + + c.p0.x += multiSubdivide_xOffset; + c.p1.x += multiSubdivide_xOffset; + c.p2.x += multiSubdivide_xOffset; + c.p3.x += multiSubdivide_xOffset; + + const double t1( 0.1+i/(3.0*sizeof(someCurves)/sizeof(Bezier)) ); + const double t2( 0.9-i/(3.0*sizeof(someCurves)/sizeof(Bezier)) ); + + // subdivide at t1 + Impl_deCasteljauAt( c1_part1, c1_part2, c, t1 ); + + // subdivide at t2_c1. As we're working on + // c1_part2 now, we have to adapt t2_c1 since + // we're no longer in the original parameter + // interval. This is based on the following + // assumption: t2_new = (t2-t1)/(1-t1), which + // relates the t2 value into the new parameter + // range [0,1] of c1_part2. + Impl_deCasteljauAt( c1_part1, c1_part3, c1_part2, (t2-t1)/(1.0-t1) ); + + // subdivide at t2 + Impl_deCasteljauAt( c1_part3, c1_part2, c, t2 ); + + std::cout << " bez(" + << c1_part1.p0.x << "," + << c1_part1.p1.x << "," + << c1_part1.p2.x << "," + << c1_part1.p3.x << ",t), bez(" + << c1_part1.p0.y+0.01 << "," + << c1_part1.p1.y+0.01 << "," + << c1_part1.p2.y+0.01 << "," + << c1_part1.p3.y+0.01 << ",t) title \"middle " << i << "\", " + << " bez(" + << c1_part2.p0.x << "," + << c1_part2.p1.x << "," + << c1_part2.p2.x << "," + << c1_part2.p3.x << ",t), bez(" + << c1_part2.p0.y << "," + << c1_part2.p1.y << "," + << c1_part2.p2.y << "," + << c1_part2.p3.y << ",t) title \"right " << i << "\", " + << " bez(" + << c1_part3.p0.x << "," + << c1_part3.p1.x << "," + << c1_part3.p2.x << "," + << c1_part3.p3.x << ",t), bez(" + << c1_part3.p0.y << "," + << c1_part3.p1.y << "," + << c1_part3.p2.y << "," + << c1_part3.p3.y << ",t) title \"left " << i << "\""; + + if( i+1<sizeof(someCurves)/sizeof(Bezier) ) + std::cout << ",\\" << std::endl; + else + std::cout << std::endl; + } +#endif + +#ifdef WITH_FATLINE_TEST + // test fatline algorithm + const double fatLine_xOffset( curr_Offset ); + curr_Offset += 20; + std::cout << "# fat line testing" << std::endl + << "plot [t=0:1] "; + for( i=0; i<sizeof(someCurves)/sizeof(Bezier); ++i ) + { + Bezier c( someCurves[i] ); + + c.p0.x += fatLine_xOffset; + c.p1.x += fatLine_xOffset; + c.p2.x += fatLine_xOffset; + c.p3.x += fatLine_xOffset; + + FatLine line; + + Impl_calcFatLine(line, c); + + std::cout << " bez(" + << c.p0.x << "," + << c.p1.x << "," + << c.p2.x << "," + << c.p3.x << ",t), bez(" + << c.p0.y << "," + << c.p1.y << "," + << c.p2.y << "," + << c.p3.y << ",t) title \"bezier " << i << "\", linex(" + << line.a << "," + << line.b << "," + << line.c << ",t), liney(" + << line.a << "," + << line.b << "," + << line.c << ",t) title \"fat line (center) on " << i << "\", linex(" + << line.a << "," + << line.b << "," + << line.c-line.dMin << ",t), liney(" + << line.a << "," + << line.b << "," + << line.c-line.dMin << ",t) title \"fat line (min) on " << i << "\", linex(" + << line.a << "," + << line.b << "," + << line.c-line.dMax << ",t), liney(" + << line.a << "," + << line.b << "," + << line.c-line.dMax << ",t) title \"fat line (max) on " << i << "\""; + + if( i+1<sizeof(someCurves)/sizeof(Bezier) ) + std::cout << ",\\" << std::endl; + else + std::cout << std::endl; + } +#endif + +#ifdef WITH_CALCFOCUS_TEST + // test focus curve algorithm + const double focus_xOffset( curr_Offset ); + curr_Offset += 20; + std::cout << "# focus line testing" << std::endl + << "plot [t=0:1] "; + for( i=0; i<sizeof(someCurves)/sizeof(Bezier); ++i ) + { + Bezier c( someCurves[i] ); + + c.p0.x += focus_xOffset; + c.p1.x += focus_xOffset; + c.p2.x += focus_xOffset; + c.p3.x += focus_xOffset; + + // calc focus curve + Bezier focus; + Impl_calcFocus(focus, c); + + std::cout << " bez(" + << c.p0.x << "," + << c.p1.x << "," + << c.p2.x << "," + << c.p3.x << ",t), bez(" + << c.p0.y << "," + << c.p1.y << "," + << c.p2.y << "," + << c.p3.y << ",t) title \"bezier " << i << "\", bez(" + << focus.p0.x << "," + << focus.p1.x << "," + << focus.p2.x << "," + << focus.p3.x << ",t), bez(" + << focus.p0.y << "," + << focus.p1.y << "," + << focus.p2.y << "," + << focus.p3.y << ",t) title \"focus " << i << "\""; + + if( i+1<sizeof(someCurves)/sizeof(Bezier) ) + std::cout << ",\\" << std::endl; + else + std::cout << std::endl; + } +#endif + +#ifdef WITH_SAFEPARAMBASE_TEST + // test safe params base method + double safeParamsBase_xOffset( curr_Offset ); + std::cout << "# safe param base method testing" << std::endl + << "plot [t=0:1] "; + for( i=0; i<sizeof(someCurves)/sizeof(Bezier); ++i ) + { + Bezier c( someCurves[i] ); + + c.p0.x += safeParamsBase_xOffset; + c.p1.x += safeParamsBase_xOffset; + c.p2.x += safeParamsBase_xOffset; + c.p3.x += safeParamsBase_xOffset; + + Polygon2D poly(4); + poly[0] = c.p0; + poly[1] = c.p1; + poly[2] = c.p2; + poly[3] = c.p3; + + double t1, t2; + + bool bRet( Impl_calcSafeParams( t1, t2, poly, 0, 1 ) ); + + Polygon2D convHull( convexHull( poly ) ); + + std::cout << " bez(" + << poly[0].x << "," + << poly[1].x << "," + << poly[2].x << "," + << poly[3].x << ",t),bez(" + << poly[0].y << "," + << poly[1].y << "," + << poly[2].y << "," + << poly[3].y << ",t), " + << "t+" << safeParamsBase_xOffset << ", 0, " + << "t+" << safeParamsBase_xOffset << ", 1, "; + if( bRet ) + { + std::cout << t1+safeParamsBase_xOffset << ", t, " + << t2+safeParamsBase_xOffset << ", t, "; + } + std::cout << "'-' using ($1):($2) title \"control polygon\" with lp, " + << "'-' using ($1):($2) title \"convex hull\" with lp"; + + if( i+1<sizeof(someCurves)/sizeof(Bezier) ) + std::cout << ",\\" << std::endl; + else + std::cout << std::endl; + + safeParamsBase_xOffset += 2; + } + + safeParamsBase_xOffset = curr_Offset; + for( i=0; i<sizeof(someCurves)/sizeof(Bezier); ++i ) + { + Bezier c( someCurves[i] ); + + c.p0.x += safeParamsBase_xOffset; + c.p1.x += safeParamsBase_xOffset; + c.p2.x += safeParamsBase_xOffset; + c.p3.x += safeParamsBase_xOffset; + + Polygon2D poly(4); + poly[0] = c.p0; + poly[1] = c.p1; + poly[2] = c.p2; + poly[3] = c.p3; + + double t1, t2; + + Impl_calcSafeParams( t1, t2, poly, 0, 1 ); + + Polygon2D convHull( convexHull( poly ) ); + + unsigned int k; + for( k=0; k<poly.size(); ++k ) + { + std::cout << poly[k].x << " " << poly[k].y << std::endl; + } + std::cout << poly[0].x << " " << poly[0].y << std::endl; + std::cout << "e" << std::endl; + + for( k=0; k<convHull.size(); ++k ) + { + std::cout << convHull[k].x << " " << convHull[k].y << std::endl; + } + std::cout << convHull[0].x << " " << convHull[0].y << std::endl; + std::cout << "e" << std::endl; + + safeParamsBase_xOffset += 2; + } + curr_Offset += 20; +#endif + +#ifdef WITH_SAFEPARAMS_TEST + // test safe parameter range algorithm + const double safeParams_xOffset( curr_Offset ); + curr_Offset += 20; + std::cout << "# safe param range testing" << std::endl + << "plot [t=0.0:1.0] "; + for( i=0; i<sizeof(someCurves)/sizeof(Bezier); ++i ) + { + for( j=i+1; j<sizeof(someCurves)/sizeof(Bezier); ++j ) + { + Bezier c1( someCurves[i] ); + Bezier c2( someCurves[j] ); + + c1.p0.x += safeParams_xOffset; + c1.p1.x += safeParams_xOffset; + c1.p2.x += safeParams_xOffset; + c1.p3.x += safeParams_xOffset; + c2.p0.x += safeParams_xOffset; + c2.p1.x += safeParams_xOffset; + c2.p2.x += safeParams_xOffset; + c2.p3.x += safeParams_xOffset; + + double t1, t2; + + if( Impl_calcClipRange(t1, t2, c1, c1, c2, c2) ) + { + // clip safe ranges off c1 + Bezier c1_part1; + Bezier c1_part2; + Bezier c1_part3; + + // subdivide at t1_c1 + Impl_deCasteljauAt( c1_part1, c1_part2, c1, t1 ); + // subdivide at t2_c1 + Impl_deCasteljauAt( c1_part1, c1_part3, c1_part2, (t2-t1)/(1.0-t1) ); + + // output remaining segment (c1_part1) + + std::cout << " bez(" + << c1.p0.x << "," + << c1.p1.x << "," + << c1.p2.x << "," + << c1.p3.x << ",t),bez(" + << c1.p0.y << "," + << c1.p1.y << "," + << c1.p2.y << "," + << c1.p3.y << ",t), bez(" + << c2.p0.x << "," + << c2.p1.x << "," + << c2.p2.x << "," + << c2.p3.x << ",t),bez(" + << c2.p0.y << "," + << c2.p1.y << "," + << c2.p2.y << "," + << c2.p3.y << ",t), bez(" + << c1_part1.p0.x << "," + << c1_part1.p1.x << "," + << c1_part1.p2.x << "," + << c1_part1.p3.x << ",t),bez(" + << c1_part1.p0.y << "," + << c1_part1.p1.y << "," + << c1_part1.p2.y << "," + << c1_part1.p3.y << ",t)"; + + if( i+2<sizeof(someCurves)/sizeof(Bezier) ) + std::cout << ",\\" << std::endl; + else + std::cout << std::endl; + } + } + } +#endif + +#ifdef WITH_SAFEPARAM_DETAILED_TEST + // test safe parameter range algorithm + const double safeParams2_xOffset( curr_Offset ); + curr_Offset += 20; + if( sizeof(someCurves)/sizeof(Bezier) > 1 ) + { + Bezier c1( someCurves[0] ); + Bezier c2( someCurves[1] ); + + c1.p0.x += safeParams2_xOffset; + c1.p1.x += safeParams2_xOffset; + c1.p2.x += safeParams2_xOffset; + c1.p3.x += safeParams2_xOffset; + c2.p0.x += safeParams2_xOffset; + c2.p1.x += safeParams2_xOffset; + c2.p2.x += safeParams2_xOffset; + c2.p3.x += safeParams2_xOffset; + + double t1, t2; + + // output happens here + Impl_calcClipRange(t1, t2, c1, c1, c2, c2); + } +#endif + +#ifdef WITH_SAFEFOCUSPARAM_TEST + // test safe parameter range from focus algorithm + const double safeParamsFocus_xOffset( curr_Offset ); + curr_Offset += 20; + std::cout << "# safe param range from focus testing" << std::endl + << "plot [t=0.0:1.0] "; + for( i=0; i<sizeof(someCurves)/sizeof(Bezier); ++i ) + { + for( j=i+1; j<sizeof(someCurves)/sizeof(Bezier); ++j ) + { + Bezier c1( someCurves[i] ); + Bezier c2( someCurves[j] ); + + c1.p0.x += safeParamsFocus_xOffset; + c1.p1.x += safeParamsFocus_xOffset; + c1.p2.x += safeParamsFocus_xOffset; + c1.p3.x += safeParamsFocus_xOffset; + c2.p0.x += safeParamsFocus_xOffset; + c2.p1.x += safeParamsFocus_xOffset; + c2.p2.x += safeParamsFocus_xOffset; + c2.p3.x += safeParamsFocus_xOffset; + + double t1, t2; + + Bezier focus; +#ifdef WITH_SAFEFOCUSPARAM_CALCFOCUS +#if 0 + { + // clip safe ranges off c1_orig + Bezier c1_part1; + Bezier c1_part2; + Bezier c1_part3; + + // subdivide at t1_c1 + Impl_deCasteljauAt( c1_part1, c1_part2, c2, 0.30204 ); + + // subdivide at t2_c1. As we're working on + // c1_part2 now, we have to adapt t2_c1 since + // we're no longer in the original parameter + // interval. This is based on the following + // assumption: t2_new = (t2-t1)/(1-t1), which + // relates the t2 value into the new parameter + // range [0,1] of c1_part2. + Impl_deCasteljauAt( c1_part1, c1_part3, c1_part2, (0.57151-0.30204)/(1.0-0.30204) ); + + c2 = c1_part1; + Impl_calcFocus( focus, c2 ); + } +#else + Impl_calcFocus( focus, c2 ); +#endif +#else + focus = c2; +#endif + // determine safe range on c1 + bool bRet( Impl_calcSafeParams_focus( t1, t2, + c1, focus ) ); + + std::cerr << "t1: " << t1 << ", t2: " << t2 << std::endl; + + // clip safe ranges off c1 + Bezier c1_part1; + Bezier c1_part2; + Bezier c1_part3; + + // subdivide at t1_c1 + Impl_deCasteljauAt( c1_part1, c1_part2, c1, t1 ); + // subdivide at t2_c1 + Impl_deCasteljauAt( c1_part1, c1_part3, c1_part2, (t2-t1)/(1.0-t1) ); + + // output remaining segment (c1_part1) + + std::cout << " bez(" + << c1.p0.x << "," + << c1.p1.x << "," + << c1.p2.x << "," + << c1.p3.x << ",t),bez(" + << c1.p0.y << "," + << c1.p1.y << "," + << c1.p2.y << "," + << c1.p3.y << ",t) title \"c1\", " +#ifdef WITH_SAFEFOCUSPARAM_CALCFOCUS + << "bez(" + << c2.p0.x << "," + << c2.p1.x << "," + << c2.p2.x << "," + << c2.p3.x << ",t),bez(" + << c2.p0.y << "," + << c2.p1.y << "," + << c2.p2.y << "," + << c2.p3.y << ",t) title \"c2\", " + << "bez(" + << focus.p0.x << "," + << focus.p1.x << "," + << focus.p2.x << "," + << focus.p3.x << ",t),bez(" + << focus.p0.y << "," + << focus.p1.y << "," + << focus.p2.y << "," + << focus.p3.y << ",t) title \"focus\""; +#else + << "bez(" + << c2.p0.x << "," + << c2.p1.x << "," + << c2.p2.x << "," + << c2.p3.x << ",t),bez(" + << c2.p0.y << "," + << c2.p1.y << "," + << c2.p2.y << "," + << c2.p3.y << ",t) title \"focus\""; +#endif + if( bRet ) + { + std::cout << ", bez(" + << c1_part1.p0.x << "," + << c1_part1.p1.x << "," + << c1_part1.p2.x << "," + << c1_part1.p3.x << ",t),bez(" + << c1_part1.p0.y+0.01 << "," + << c1_part1.p1.y+0.01 << "," + << c1_part1.p2.y+0.01 << "," + << c1_part1.p3.y+0.01 << ",t) title \"part\""; + } + + if( i+2<sizeof(someCurves)/sizeof(Bezier) ) + std::cout << ",\\" << std::endl; + else + std::cout << std::endl; + } + } +#endif + +#ifdef WITH_SAFEFOCUSPARAM_DETAILED_TEST + // test safe parameter range algorithm + const double safeParams3_xOffset( curr_Offset ); + curr_Offset += 20; + if( sizeof(someCurves)/sizeof(Bezier) > 1 ) + { + Bezier c1( someCurves[0] ); + Bezier c2( someCurves[1] ); + + c1.p0.x += safeParams3_xOffset; + c1.p1.x += safeParams3_xOffset; + c1.p2.x += safeParams3_xOffset; + c1.p3.x += safeParams3_xOffset; + c2.p0.x += safeParams3_xOffset; + c2.p1.x += safeParams3_xOffset; + c2.p2.x += safeParams3_xOffset; + c2.p3.x += safeParams3_xOffset; + + double t1, t2; + + Bezier focus; +#ifdef WITH_SAFEFOCUSPARAM_CALCFOCUS + Impl_calcFocus( focus, c2 ); +#else + focus = c2; +#endif + + // determine safe range on c1, output happens here + Impl_calcSafeParams_focus( t1, t2, + c1, focus ); + } +#endif + +#ifdef WITH_BEZIERCLIP_TEST + std::vector< std::pair<double, double> > result; + std::back_insert_iterator< std::vector< std::pair<double, double> > > ii(result); + + // test full bezier clipping + const double bezierClip_xOffset( curr_Offset ); + std::cout << std::endl << std::endl << "# bezier clip testing" << std::endl + << "plot [t=0:1] "; + for( i=0; i<sizeof(someCurves)/sizeof(Bezier); ++i ) + { + for( j=i+1; j<sizeof(someCurves)/sizeof(Bezier); ++j ) + { + Bezier c1( someCurves[i] ); + Bezier c2( someCurves[j] ); + + c1.p0.x += bezierClip_xOffset; + c1.p1.x += bezierClip_xOffset; + c1.p2.x += bezierClip_xOffset; + c1.p3.x += bezierClip_xOffset; + c2.p0.x += bezierClip_xOffset; + c2.p1.x += bezierClip_xOffset; + c2.p2.x += bezierClip_xOffset; + c2.p3.x += bezierClip_xOffset; + + std::cout << " bez(" + << c1.p0.x << "," + << c1.p1.x << "," + << c1.p2.x << "," + << c1.p3.x << ",t),bez(" + << c1.p0.y << "," + << c1.p1.y << "," + << c1.p2.y << "," + << c1.p3.y << ",t), bez(" + << c2.p0.x << "," + << c2.p1.x << "," + << c2.p2.x << "," + << c2.p3.x << ",t),bez(" + << c2.p0.y << "," + << c2.p1.y << "," + << c2.p2.y << "," + << c2.p3.y << ",t), '-' using (bez(" + << c1.p0.x << "," + << c1.p1.x << "," + << c1.p2.x << "," + << c1.p3.x + << ",$1)):(bez(" + << c1.p0.y << "," + << c1.p1.y << "," + << c1.p2.y << "," + << c1.p3.y << ",$1)) title \"bezier " << i << " clipped against " << j << " (t on " << i << ")\", " + << " '-' using (bez(" + << c2.p0.x << "," + << c2.p1.x << "," + << c2.p2.x << "," + << c2.p3.x + << ",$1)):(bez(" + << c2.p0.y << "," + << c2.p1.y << "," + << c2.p2.y << "," + << c2.p3.y << ",$1)) title \"bezier " << i << " clipped against " << j << " (t on " << j << ")\""; + + if( i+2<sizeof(someCurves)/sizeof(Bezier) ) + std::cout << ",\\" << std::endl; + else + std::cout << std::endl; + } + } + for( i=0; i<sizeof(someCurves)/sizeof(Bezier); ++i ) + { + for( j=i+1; j<sizeof(someCurves)/sizeof(Bezier); ++j ) + { + result.clear(); + Bezier c1( someCurves[i] ); + Bezier c2( someCurves[j] ); + + c1.p0.x += bezierClip_xOffset; + c1.p1.x += bezierClip_xOffset; + c1.p2.x += bezierClip_xOffset; + c1.p3.x += bezierClip_xOffset; + c2.p0.x += bezierClip_xOffset; + c2.p1.x += bezierClip_xOffset; + c2.p2.x += bezierClip_xOffset; + c2.p3.x += bezierClip_xOffset; + + clipBezier( ii, 0.00001, c1, c2 ); + + for( k=0; k<result.size(); ++k ) + { + std::cout << result[k].first << std::endl; + } + std::cout << "e" << std::endl; + + for( k=0; k<result.size(); ++k ) + { + std::cout << result[k].second << std::endl; + } + std::cout << "e" << std::endl; + } + } +#endif + + return 0; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/basegfx/source/workbench/bezierclip.hxx b/basegfx/source/workbench/bezierclip.hxx new file mode 100644 index 0000000000..54cceb414f --- /dev/null +++ b/basegfx/source/workbench/bezierclip.hxx @@ -0,0 +1,84 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * 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 . + */ + +#pragma once + +#include <vector> + +struct Point2D +{ + typedef double value_type; + Point2D( double _x, double _y ) : x(_x), y(_y) {} + Point2D() : x(), y() {} + double x; + double y; +}; + +struct Bezier +{ + Point2D p0; + Point2D p1; + Point2D p2; + Point2D p3; + + Point2D& operator[]( int i ) { return reinterpret_cast<Point2D*>(this)[i]; } + const Point2D& operator[]( int i ) const { return reinterpret_cast<const Point2D*>(this)[i]; } +}; + +struct FatLine +{ + // line L through p1 and p4 in normalized implicit form + double a; + double b; + double c; + + // the upper and lower distance from this line + double dMin; + double dMax; +}; + +template <typename DataType> DataType calcLineDistance( const DataType& a, + const DataType& b, + const DataType& c, + const DataType& x, + const DataType& y ) +{ + return a*x + b*y + c; +} + +typedef std::vector< Point2D > Polygon2D; + +/* little abs template */ +template <typename NumType> NumType absval( NumType x ) +{ + return x<0 ? -x : x; +} + +Polygon2D convexHull( const Polygon2D& rPoly ); + +// TODO: find proper epsilon here (try std::numeric_limits<NumType>::epsilon()?)! +constexpr auto DBL_EPSILON = 1.0e-100; + +/* little approximate comparisons */ +template <typename NumType> bool tolZero( NumType n ) { return fabs(n) < DBL_EPSILON; } +template <typename NumType> bool tolEqual( NumType n1, NumType n2 ) { return tolZero(n1-n2); } +template <typename NumType> bool tolLessEqual( NumType n1, NumType n2 ) { return tolEqual(n1,n2) || n1<n2; } +template <typename NumType> bool tolGreaterEqual( NumType n1, NumType n2 ) { return tolEqual(n1,n2) || n1>n2; } + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/basegfx/source/workbench/convexhull.cxx b/basegfx/source/workbench/convexhull.cxx new file mode 100644 index 0000000000..ddcb040465 --- /dev/null +++ b/basegfx/source/workbench/convexhull.cxx @@ -0,0 +1,200 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * 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 . + */ + +#include <algorithm> +#include <vector> + +#include <bezierclip.hxx> + +/* Implements the theta function from Sedgewick: Algorithms in XXX, chapter 24 */ +template <class PointType> double theta( const PointType& p1, const PointType& p2 ) +{ + typename PointType::value_type dx, dy, ax, ay; + double t; + + dx = p2.x - p1.x; ax = absval( dx ); + dy = p2.y - p1.y; ay = absval( dy ); + t = (ax+ay == 0) ? 0 : (double) dy/(ax+ay); + if( dx < 0 ) + t = 2-t; + else if( dy < 0 ) + t = 4+t; + + return t*90.0; +} + +/* Model of LessThanComparable for theta sort. + * Uses the theta function from Sedgewick: Algorithms in XXX, chapter 24 + */ +template <class PointType> class ThetaCompare +{ +public: + explicit ThetaCompare( const PointType& rRefPoint ) : maRefPoint( rRefPoint ) {} + + bool operator() ( const PointType& p1, const PointType& p2 ) + { + // return true, if p1 precedes p2 in the angle relative to maRefPoint + return theta(maRefPoint, p1) < theta(maRefPoint, p2); + } + + double operator() ( const PointType& p ) const + { + return theta(maRefPoint, p); + } + +private: + PointType maRefPoint; +}; + +/* Implementation of the predicate 'counter-clockwise' for three points, from Sedgewick: Algorithms in XXX, chapter 24 */ +template <class PointType, class CmpFunctor> typename PointType::value_type ccw( const PointType& p0, const PointType& p1, const PointType& p2, CmpFunctor& thetaCmp ) +{ + typename PointType::value_type dx1, dx2, dy1, dy2; + typename PointType::value_type theta0( thetaCmp(p0) ); + typename PointType::value_type theta1( thetaCmp(p1) ); + typename PointType::value_type theta2( thetaCmp(p2) ); + +#if 0 + if( theta0 == theta1 || + theta0 == theta2 || + theta1 == theta2 ) + { + // cannot reliably compare, as at least two points are + // theta-equal. See bug description below + return 0; + } +#endif + + dx1 = p1.x - p0.x; dy1 = p1.y - p0.y; + dx2 = p2.x - p0.x; dy2 = p2.y - p0.y; + + if( dx1*dy2 > dy1*dx2 ) + return +1; + + if( dx1*dy2 < dy1*dx2 ) + return -1; + + if( (dx1*dx2 < 0) || (dy1*dy2 < 0) ) + return -1; + + if( (dx1*dx1 + dy1*dy1) < (dx2*dx2 + dy2*dy2) ) + return +1; + + return 0; +} + +/* + Bug + === + + Sometimes, the resulting polygon is not the convex hull (see below + for an edge configuration to reproduce that problem) + + Problem analysis: + ================= + + The root cause of this bug is the fact that the second part of + the algorithm (the 'wrapping' of the point set) relies on the + previous theta sorting. Namely, it is required that the + generated point ordering, when interpreted as a polygon, is not + self-intersecting. This property, although, cannot be + guaranteed due to limited floating point accuracy. For example, + for two points very close together, and at the same time very + far away from the theta reference point p1, can appear on the + same theta value (because floating point accuracy is limited), + although on different rays to p1 when inspected locally. + + Example: + + / + P3 / + |\ / + | / + |/ \ + P2 \ + \ + ...____________\ + P1 + + Here, P2 and P3 are theta-equal relative to P1, but the local + ccw measure always says 'left turn'. Thus, the convex hull is + wrong at this place. + + Solution: + ========= + + If two points are theta-equal and checked via ccw, ccw must + also classify them as 'equal'. Thus, the second stage of the + convex hull algorithm sorts the first one out, effectively + reducing a cluster of theta-equal points to only one. This + single point can then be treated correctly. +*/ + +/* Implementation of Graham's convex hull algorithm, see Sedgewick: Algorithms in XXX, chapter 25 */ +Polygon2D convexHull( const Polygon2D& rPoly ) +{ + const Polygon2D::size_type N( rPoly.size() ); + Polygon2D result( N + 1 ); + std::copy(rPoly.begin(), rPoly.end(), result.begin()+1 ); + Polygon2D::size_type min, i; + + // determine safe point on hull (smallest y value) + for( min=1, i=2; i<=N; ++i ) + { + if( result[i].y < result[min].y ) + min = i; + } + + // determine safe point on hull (largest x value) + for( i=1; i<=N; ++i ) + { + if( result[i].y == result[min].y && + result[i].x > result[min].x ) + { + min = i; + } + } + + // TODO: add inner elimination optimization from Sedgewick: Algorithms in XXX, chapter 25 + // TODO: use radix sort instead of std::sort(), calc theta only once and store + + // setup first point and sort + std::swap( result[1], result[min] ); + ThetaCompare<Point2D> cmpFunc(result[1]); + std::sort( result.begin()+1, result.end(), cmpFunc ); + + // setup sentinel + result[0] = result[N]; + + // generate convex hull + Polygon2D::size_type M; + for( M=3, i=4; i<=N; ++i ) + { + while( ccw(result[M], result[M-1], result[i], cmpFunc) >= 0 ) + --M; + + ++M; + std::swap( result[M], result[i] ); + } + + // copy range [1,M] to output + return Polygon2D( result.begin()+1, result.begin()+M+1 ); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/basegfx/source/workbench/gauss.hxx b/basegfx/source/workbench/gauss.hxx new file mode 100644 index 0000000000..4ef050ccbc --- /dev/null +++ b/basegfx/source/workbench/gauss.hxx @@ -0,0 +1,163 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * 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 . + */ + +/** This method eliminates elements below main diagonal in the given + matrix by gaussian elimination. + + @param matrix + The matrix to operate on. Last column is the result vector (right + hand side of the linear equation). After successful termination, + the matrix is upper triangular. The matrix is expected to be in + row major order. + + @param rows + Number of rows in matrix + + @param cols + Number of columns in matrix + + @param minPivot + If the pivot element gets lesser than minPivot, this method fails, + otherwise, elimination succeeds and true is returned. + + @return true, if elimination succeeded. + */ + +#pragma once + +template <class Matrix, typename BaseType> +bool eliminate( Matrix& matrix, + int rows, + int cols, + const BaseType& minPivot ) +{ + /* i, j, k *must* be signed, when looping like: j>=0 ! */ + /* eliminate below main diagonal */ + for(int i=0; i<cols-1; ++i) + { + /* find best pivot */ + int max = i; + for(int j=i+1; j<rows; ++j) + if( fabs(matrix[ j*cols + i ]) > fabs(matrix[ max*cols + i ]) ) + max = j; + + /* check pivot value */ + if( fabs(matrix[ max*cols + i ]) < minPivot ) + return false; /* pivot too small! */ + + /* interchange rows 'max' and 'i' */ + for(int k=0; k<cols; ++k) + { + std::swap(matrix[ i*cols + k ], matrix[ max*cols + k ]); + } + + /* eliminate column */ + for(int j=i+1; j<rows; ++j) + for(int k=cols-1; k>=i; --k) + matrix[ j*cols + k ] -= matrix[ i*cols + k ] * + matrix[ j*cols + i ] / matrix[ i*cols + i ]; + } + + /* everything went well */ + return true; +} + +/** Retrieve solution vector of linear system by substituting backwards. + + This operation _relies_ on the previous successful + application of eliminate()! + + @param matrix + Matrix in upper diagonal form, as e.g. generated by eliminate() + + @param rows + Number of rows in matrix + + @param cols + Number of columns in matrix + + @param result + Result vector. Given matrix must have space for one column (rows entries). + + @return true, if back substitution was possible (i.e. no division + by zero occurred). + */ +template <class Matrix, class Vector, typename BaseType> +bool substitute( const Matrix& matrix, + int rows, + int cols, + Vector& result ) +{ + BaseType temp; + + /* j, k *must* be signed, when looping like: j>=0 ! */ + /* substitute backwards */ + for(int j=rows-1; j>=0; --j) + { + temp = 0.0; + for(int k=j+1; k<cols-1; ++k) + temp += matrix[ j*cols + k ] * result[k]; + + if( matrix[ j*cols + j ] == 0.0 ) + return false; /* imminent division by zero! */ + + result[j] = (matrix[ j*cols + cols-1 ] - temp) / matrix[ j*cols + j ]; + } + + /* everything went well */ + return true; +} + +/** This method determines solution of given linear system, if any + + This is a wrapper for eliminate and substitute, given matrix must + contain right side of equation as the last column. + + @param matrix + The matrix to operate on. Last column is the result vector (right + hand side of the linear equation). After successful termination, + the matrix is upper triangular. The matrix is expected to be in + row major order. + + @param rows + Number of rows in matrix + + @param cols + Number of columns in matrix + + @param minPivot + If the pivot element gets lesser than minPivot, this method fails, + otherwise, elimination succeeds and true is returned. + + @return true, if elimination succeeded. + */ +template <class Matrix, class Vector, typename BaseType> +bool solve( Matrix& matrix, + int rows, + int cols, + Vector& result, + BaseType minPivot ) +{ + if( eliminate<Matrix,BaseType>(matrix, rows, cols, minPivot) ) + return substitute<Matrix,Vector,BaseType>(matrix, rows, cols, result); + + return false; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ |