calc: on editing invalidation of view with different zoom is wrong
[LibreOffice.git] / drawinglayer / source / primitive2d / glowprimitive2d.cxx
blob47103ac9a0117592cd8a512cd1708cf181dd711b
1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2 /*
3 * This file is part of the LibreOffice project.
5 * This Source Code Form is subject to the terms of the Mozilla Public
6 * License, v. 2.0. If a copy of the MPL was not distributed with this
7 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
9 * This file incorporates work covered by the following license notice:
11 * Licensed to the Apache Software Foundation (ASF) under one or more
12 * contributor license agreements. See the NOTICE file distributed
13 * with this work for additional information regarding copyright
14 * ownership. The ASF licenses this file to you under the Apache
15 * License, Version 2.0 (the "License"); you may not use this file
16 * except in compliance with the License. You may obtain a copy of
17 * the License at http://www.apache.org/licenses/LICENSE-2.0 .
20 #include <drawinglayer/primitive2d/glowprimitive2d.hxx>
21 #include <drawinglayer/primitive2d/transformprimitive2d.hxx>
22 #include <drawinglayer/primitive2d/drawinglayer_primitivetypes2d.hxx>
23 #include <drawinglayer/primitive2d/bitmapprimitive2d.hxx>
24 #include <basegfx/matrix/b2dhommatrixtools.hxx>
25 #include <toolkit/helper/vclunohelper.hxx>
26 #include <drawinglayer/converters.hxx>
27 #include "GlowSoftEgdeShadowTools.hxx"
29 #ifdef DBG_UTIL
30 #include <tools/stream.hxx>
31 #include <vcl/filter/PngImageWriter.hxx>
32 #endif
34 using namespace com::sun::star;
36 namespace drawinglayer::primitive2d
38 GlowPrimitive2D::GlowPrimitive2D(const Color& rGlowColor, double fRadius,
39 Primitive2DContainer&& rChildren)
40 : BufferedDecompositionGroupPrimitive2D(std::move(rChildren))
41 , maGlowColor(rGlowColor)
42 , mfGlowRadius(fRadius)
43 , mfLastDiscreteGlowRadius(0.0)
44 , maLastClippedRange()
48 bool GlowPrimitive2D::operator==(const BasePrimitive2D& rPrimitive) const
50 if (BufferedDecompositionGroupPrimitive2D::operator==(rPrimitive))
52 const GlowPrimitive2D& rCompare = static_cast<const GlowPrimitive2D&>(rPrimitive);
54 return (getGlowRadius() == rCompare.getGlowRadius()
55 && getGlowColor() == rCompare.getGlowColor());
58 return false;
61 bool GlowPrimitive2D::prepareValuesAndcheckValidity(
62 basegfx::B2DRange& rGlowRange, basegfx::B2DRange& rClippedRange,
63 basegfx::B2DVector& rDiscreteGlowSize, double& rfDiscreteGlowRadius,
64 const geometry::ViewInformation2D& rViewInformation) const
66 // no GlowRadius defined, done
67 if (getGlowRadius() <= 0.0)
68 return false;
70 // no geometry, done
71 if (getChildren().empty())
72 return false;
74 // no pixel target, done
75 if (rViewInformation.getObjectToViewTransformation().isIdentity())
76 return false;
78 // get geometry range that defines area that needs to be pixelated
79 rGlowRange = getChildren().getB2DRange(rViewInformation);
81 // no range of geometry, done
82 if (rGlowRange.isEmpty())
83 return false;
85 // extend range by GlowRadius in all directions
86 rGlowRange.grow(getGlowRadius());
88 // initialize ClippedRange to full GlowRange -> all is visible
89 rClippedRange = rGlowRange;
91 // get Viewport and check if used. If empty, all is visible (see
92 // ViewInformation2D definition in viewinformation2d.hxx)
93 if (!rViewInformation.getViewport().isEmpty())
95 // if used, extend by GlowRadius to ensure needed parts are included
96 basegfx::B2DRange aVisibleArea(rViewInformation.getViewport());
97 aVisibleArea.grow(getGlowRadius());
99 // calculate ClippedRange
100 rClippedRange.intersect(aVisibleArea);
102 // if GlowRange is completely outside of VisibleArea, ClippedRange
103 // will be empty and we are done
104 if (rClippedRange.isEmpty())
105 return false;
108 // calculate discrete pixel size of GlowRange. If it's too small to visualize, we are done
109 rDiscreteGlowSize = rViewInformation.getObjectToViewTransformation() * rGlowRange.getRange();
110 if (ceil(rDiscreteGlowSize.getX()) < 2.0 || ceil(rDiscreteGlowSize.getY()) < 2.0)
111 return false;
113 // calculate discrete pixel size of GlowRadius. If it's too small to visualize, we are done
114 rfDiscreteGlowRadius = ceil(
115 (rViewInformation.getObjectToViewTransformation() * basegfx::B2DVector(getGlowRadius(), 0))
116 .getLength());
117 if (rfDiscreteGlowRadius < 1.0)
118 return false;
120 return true;
123 void GlowPrimitive2D::create2DDecomposition(
124 Primitive2DContainer& rContainer, const geometry::ViewInformation2D& rViewInformation) const
126 basegfx::B2DRange aGlowRange;
127 basegfx::B2DRange aClippedRange;
128 basegfx::B2DVector aDiscreteGlowSize;
129 double fDiscreteGlowRadius(0.0);
131 // Check various validity details and calculate/prepare values. If false, we are done
132 if (!prepareValuesAndcheckValidity(aGlowRange, aClippedRange, aDiscreteGlowSize,
133 fDiscreteGlowRadius, rViewInformation))
134 return;
136 // Create embedding transformation from object to top-left zero-aligned
137 // target pixel geometry (discrete form of ClippedRange)
138 // First, move to top-left of GlowRange
139 const sal_uInt32 nDiscreteGlowWidth(ceil(aDiscreteGlowSize.getX()));
140 const sal_uInt32 nDiscreteGlowHeight(ceil(aDiscreteGlowSize.getY()));
141 basegfx::B2DHomMatrix aEmbedding(basegfx::utils::createTranslateB2DHomMatrix(
142 -aClippedRange.getMinX(), -aClippedRange.getMinY()));
143 // Second, scale to discrete bitmap size
144 // Even when using the offset from ClippedRange, we need to use the
145 // scaling from the full representation, thus from GlowRange
146 aEmbedding.scale(nDiscreteGlowWidth / aGlowRange.getWidth(),
147 nDiscreteGlowHeight / aGlowRange.getHeight());
149 // Embed content graphics to TransformPrimitive2D
150 const primitive2d::Primitive2DReference xEmbedRef(
151 new primitive2d::TransformPrimitive2D(aEmbedding, Primitive2DContainer(getChildren())));
152 primitive2d::Primitive2DContainer xEmbedSeq{ xEmbedRef };
154 // Create BitmapEx using drawinglayer tooling, including a MaximumQuadraticPixel
155 // limitation to be safe and not go runtime/memory havoc. Use a pretty small
156 // limit due to this is glow functionality and will look good with bitmap scaling
157 // anyways. The value of 250.000 square pixels below maybe adapted as needed.
158 const basegfx::B2DVector aDiscreteClippedSize(rViewInformation.getObjectToViewTransformation()
159 * aClippedRange.getRange());
160 const sal_uInt32 nDiscreteClippedWidth(ceil(aDiscreteClippedSize.getX()));
161 const sal_uInt32 nDiscreteClippedHeight(ceil(aDiscreteClippedSize.getY()));
162 const geometry::ViewInformation2D aViewInformation2D;
163 const sal_uInt32 nMaximumQuadraticPixels(250000);
165 // I have now added a helper that just creates the mask without having
166 // to render the content, use it, it's faster
167 const AlphaMask aAlpha(::drawinglayer::createAlphaMask(
168 std::move(xEmbedSeq), aViewInformation2D, nDiscreteClippedWidth, nDiscreteClippedHeight,
169 nMaximumQuadraticPixels));
171 if (!aAlpha.IsEmpty())
173 const Size& rBitmapExSizePixel(aAlpha.GetSizePixel());
175 if (rBitmapExSizePixel.Width() > 0 && rBitmapExSizePixel.Height() > 0)
177 // We may have to take a corrective scaling into account when the
178 // MaximumQuadraticPixel limit was used/triggered
179 double fScale(1.0);
181 if (static_cast<sal_uInt32>(rBitmapExSizePixel.Width()) != nDiscreteClippedWidth
182 || static_cast<sal_uInt32>(rBitmapExSizePixel.Height()) != nDiscreteClippedHeight)
184 // scale in X and Y should be the same (see fReduceFactor in createAlphaMask),
185 // so adapt numerically to a single scale value, they are integer rounded values
186 const double fScaleX(static_cast<double>(rBitmapExSizePixel.Width())
187 / static_cast<double>(nDiscreteClippedWidth));
188 const double fScaleY(static_cast<double>(rBitmapExSizePixel.Height())
189 / static_cast<double>(nDiscreteClippedHeight));
191 fScale = (fScaleX + fScaleY) * 0.5;
194 // fDiscreteGlowRadius is the size of the halo from each side of the object. The halo is the
195 // border of glow color that fades from glow transparency level to fully transparent
196 // When blurring a sharp boundary (our case), it gets 50% of original intensity, and
197 // fades to both sides by the blur radius; thus blur radius is half of glow radius.
198 // Consider glow transparency (initial transparency near the object edge)
199 const AlphaMask mask(ProcessAndBlurAlphaMask(aAlpha, fDiscreteGlowRadius * fScale / 2.0,
200 fDiscreteGlowRadius * fScale / 2.0,
201 255 - getGlowColor().GetAlpha()));
203 // The end result is the bitmap filled with glow color and blurred 8-bit alpha mask
204 Bitmap bmp(aAlpha.GetSizePixel(), vcl::PixelFormat::N24_BPP);
205 bmp.Erase(getGlowColor());
206 BitmapEx result(bmp, mask);
208 #ifdef DBG_UTIL
209 static bool bDoSaveForVisualControl(false); // loplugin:constvars:ignore
210 if (bDoSaveForVisualControl)
212 // VCL_DUMP_BMP_PATH should be like C:/path/ or ~/path/
213 static const OUString sDumpPath(
214 OUString::createFromAscii(std::getenv("VCL_DUMP_BMP_PATH")));
215 if (!sDumpPath.isEmpty())
217 SvFileStream aNew(sDumpPath + "test_glow.png",
218 StreamMode::WRITE | StreamMode::TRUNC);
219 vcl::PngImageWriter aPNGWriter(aNew);
220 aPNGWriter.write(result);
223 #endif
225 // Independent from discrete sizes of glow alpha creation, always
226 // map and project glow result to geometry range extended by glow
227 // radius, but to the eventually clipped instance (ClippedRange)
228 const primitive2d::Primitive2DReference xEmbedRefBitmap(new BitmapPrimitive2D(
229 result, basegfx::utils::createScaleTranslateB2DHomMatrix(
230 aClippedRange.getWidth(), aClippedRange.getHeight(),
231 aClippedRange.getMinX(), aClippedRange.getMinY())));
233 rContainer = primitive2d::Primitive2DContainer{ xEmbedRefBitmap };
238 // Using tooling class BufferedDecompositionGroupPrimitive2D now, so
239 // no more need to locally do the buffered get2DDecomposition here,
240 // see BufferedDecompositionGroupPrimitive2D::get2DDecomposition
241 void GlowPrimitive2D::get2DDecomposition(Primitive2DDecompositionVisitor& rVisitor,
242 const geometry::ViewInformation2D& rViewInformation) const
244 basegfx::B2DRange aGlowRange;
245 basegfx::B2DRange aClippedRange;
246 basegfx::B2DVector aDiscreteGlowSize;
247 double fDiscreteGlowRadius(0.0);
249 // Check various validity details and calculate/prepare values. If false, we are done
250 if (!prepareValuesAndcheckValidity(aGlowRange, aClippedRange, aDiscreteGlowSize,
251 fDiscreteGlowRadius, rViewInformation))
252 return;
254 if (!getBuffered2DDecomposition().empty())
256 // First check is to detect if the last created decompose is capable
257 // to represent the now requested visualization.
258 // ClippedRange is the needed visualizationArea for the current glow
259 // effect, LastClippedRange is the one from the existing/last rendering.
260 // Check if last created area is sufficient and can be re-used
261 if (!maLastClippedRange.isEmpty() && !maLastClippedRange.isInside(aClippedRange))
263 // To avoid unnecessary invalidations due to being *very* correct
264 // with HairLines (which are view-dependent and thus change the
265 // result(s) here slightly when changing zoom), add a slight unsharp
266 // component if we have a ViewTransform. The derivation is inside
267 // the range of half a pixel (due to one pixel hairline)
268 basegfx::B2DRange aLastClippedRangeAndHairline(maLastClippedRange);
270 if (!rViewInformation.getObjectToViewTransformation().isIdentity())
272 // Grow by view-dependent size of 1/2 pixel
273 const double fHalfPixel((rViewInformation.getInverseObjectToViewTransformation()
274 * basegfx::B2DVector(0.5, 0))
275 .getLength());
276 aLastClippedRangeAndHairline.grow(fHalfPixel);
279 if (!aLastClippedRangeAndHairline.isInside(aClippedRange))
281 // Conditions of last local decomposition have changed, delete
282 const_cast<GlowPrimitive2D*>(this)->setBuffered2DDecomposition(
283 Primitive2DContainer());
288 if (!getBuffered2DDecomposition().empty())
290 // Second check is to react on changes of the DiscreteGlowRadius when
291 // zooming in/out.
292 // Use the known last and current DiscreteGlowRadius to decide
293 // if the visualization can be re-used. Be a little 'creative' here
294 // and make it dependent on a *relative* change - it is not necessary
295 // to re-create everytime if the exact value is missed since zooming
296 // pixel-based glow effect is pretty good due to it's smooth nature
297 bool bFree(mfLastDiscreteGlowRadius <= 0.0 || fDiscreteGlowRadius <= 0.0);
299 if (!bFree)
301 const double fDiff(fabs(mfLastDiscreteGlowRadius - fDiscreteGlowRadius));
302 const double fLen(fabs(mfLastDiscreteGlowRadius) + fabs(fDiscreteGlowRadius));
303 const double fRelativeChange(fDiff / fLen);
305 // Use lower fixed values here to change more often, higher to change less often.
306 // Value is in the range of ]0.0 .. 1.0]
307 bFree = fRelativeChange >= 0.15;
310 if (bFree)
312 // Conditions of last local decomposition have changed, delete
313 const_cast<GlowPrimitive2D*>(this)->setBuffered2DDecomposition(Primitive2DContainer());
317 if (getBuffered2DDecomposition().empty())
319 // refresh last used DiscreteGlowRadius and ClippedRange to new remembered values
320 const_cast<GlowPrimitive2D*>(this)->mfLastDiscreteGlowRadius = fDiscreteGlowRadius;
321 const_cast<GlowPrimitive2D*>(this)->maLastClippedRange = aClippedRange;
324 // call parent, that will check for empty, call create2DDecomposition and
325 // set as decomposition
326 BufferedDecompositionGroupPrimitive2D::get2DDecomposition(rVisitor, rViewInformation);
329 basegfx::B2DRange
330 GlowPrimitive2D::getB2DRange(const geometry::ViewInformation2D& rViewInformation) const
332 // Hint: Do *not* use GroupPrimitive2D::getB2DRange, that will (unnecessarily)
333 // use the decompose - what works, but is not needed here.
334 // We know the to-be-visualized geometry and the radius it needs to be extended,
335 // so simply calculate the exact needed range.
336 basegfx::B2DRange aRetval(getChildren().getB2DRange(rViewInformation));
338 // We need additional space for the glow from all sides
339 aRetval.grow(getGlowRadius());
341 return aRetval;
344 // provide unique ID
345 sal_uInt32 GlowPrimitive2D::getPrimitive2DID() const { return PRIMITIVE2D_ID_GLOWPRIMITIVE2D; }
347 } // end of namespace
349 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */