gecko-dev/gfx/2d/PathD2D.cpp
Timothy Nikkel 1a15b46e51 Bug 1847149. Make PathBuilderD2D::Arc handle all start and end angles properly. r=lsalzman
This steals some code from ArcToBezier https://searchfox.org/mozilla-central/rev/fb43eb3bdf5b51000bc7dfe3474cbe56ca2ab63c/gfx/2d/PathHelpers.h#127 so that the start and end angle are within 2*Pi and so that they are in the correct numerical order depending if we are sweeping clockwise or anticlockwise. This makes calculations easier. Specifically it makes the determination of arc size (more or less than Pi) work below. This replaces some code which tried to do something similar but did not account for all cases.

Differential Revision: https://phabricator.services.mozilla.com/D185517
2023-08-07 02:05:30 +00:00

431 lines
13 KiB
C++

/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* 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 "PathD2D.h"
#include "HelpersD2D.h"
#include <math.h>
#include "DrawTargetD2D1.h"
#include "Logging.h"
#include "PathHelpers.h"
namespace mozilla {
namespace gfx {
already_AddRefed<PathBuilder> PathBuilderD2D::Create(FillRule aFillRule) {
RefPtr<ID2D1PathGeometry> path;
HRESULT hr =
DrawTargetD2D1::factory()->CreatePathGeometry(getter_AddRefs(path));
if (FAILED(hr)) {
gfxWarning() << "Failed to create Direct2D Path Geometry. Code: "
<< hexa(hr);
return nullptr;
}
RefPtr<ID2D1GeometrySink> sink;
hr = path->Open(getter_AddRefs(sink));
if (FAILED(hr)) {
gfxWarning() << "Failed to access Direct2D Path Geometry. Code: "
<< hexa(hr);
return nullptr;
}
if (aFillRule == FillRule::FILL_WINDING) {
sink->SetFillMode(D2D1_FILL_MODE_WINDING);
}
return MakeAndAddRef<PathBuilderD2D>(sink, path, aFillRule,
BackendType::DIRECT2D1_1);
}
// This class exists as a wrapper for ID2D1SimplifiedGeometry sink, it allows
// a geometry to be duplicated into a geometry sink, while removing the final
// figure end and thus allowing a figure that was implicitly closed to be
// continued.
class OpeningGeometrySink : public ID2D1SimplifiedGeometrySink {
public:
explicit OpeningGeometrySink(ID2D1SimplifiedGeometrySink* aSink)
: mSink(aSink), mNeedsFigureEnded(false) {}
HRESULT STDMETHODCALLTYPE QueryInterface(const IID& aIID, void** aPtr) {
if (!aPtr) {
return E_POINTER;
}
if (aIID == IID_IUnknown) {
*aPtr = static_cast<IUnknown*>(this);
return S_OK;
} else if (aIID == IID_ID2D1SimplifiedGeometrySink) {
*aPtr = static_cast<ID2D1SimplifiedGeometrySink*>(this);
return S_OK;
}
return E_NOINTERFACE;
}
ULONG STDMETHODCALLTYPE AddRef() { return 1; }
ULONG STDMETHODCALLTYPE Release() { return 1; }
// We ignore SetFillMode, the copier will decide.
STDMETHOD_(void, SetFillMode)(D2D1_FILL_MODE aMode) {
EnsureFigureEnded();
return;
}
STDMETHOD_(void, BeginFigure)
(D2D1_POINT_2F aPoint, D2D1_FIGURE_BEGIN aBegin) {
EnsureFigureEnded();
return mSink->BeginFigure(aPoint, aBegin);
}
STDMETHOD_(void, AddLines)(const D2D1_POINT_2F* aLines, UINT aCount) {
EnsureFigureEnded();
return mSink->AddLines(aLines, aCount);
}
STDMETHOD_(void, AddBeziers)
(const D2D1_BEZIER_SEGMENT* aSegments, UINT aCount) {
EnsureFigureEnded();
return mSink->AddBeziers(aSegments, aCount);
}
STDMETHOD(Close)() { /* Should never be called! */
return S_OK;
}
STDMETHOD_(void, SetSegmentFlags)(D2D1_PATH_SEGMENT aFlags) {
return mSink->SetSegmentFlags(aFlags);
}
// This function is special - it's the reason this class exists.
// It needs to intercept the very last endfigure. So that a user can
// continue writing to this sink as if they never stopped.
STDMETHOD_(void, EndFigure)(D2D1_FIGURE_END aEnd) {
if (aEnd == D2D1_FIGURE_END_CLOSED) {
return mSink->EndFigure(aEnd);
} else {
mNeedsFigureEnded = true;
}
}
private:
void EnsureFigureEnded() {
if (mNeedsFigureEnded) {
mSink->EndFigure(D2D1_FIGURE_END_OPEN);
mNeedsFigureEnded = false;
}
}
ID2D1SimplifiedGeometrySink* mSink;
bool mNeedsFigureEnded;
};
PathBuilderD2D::~PathBuilderD2D() {}
void PathBuilderD2D::MoveTo(const Point& aPoint) {
if (mFigureActive) {
mSink->EndFigure(D2D1_FIGURE_END_OPEN);
mFigureActive = false;
}
EnsureActive(aPoint);
mCurrentPoint = aPoint;
}
void PathBuilderD2D::LineTo(const Point& aPoint) {
EnsureActive(aPoint);
mSink->AddLine(D2DPoint(aPoint));
mCurrentPoint = aPoint;
mFigureEmpty = false;
}
void PathBuilderD2D::BezierTo(const Point& aCP1, const Point& aCP2,
const Point& aCP3) {
EnsureActive(aCP1);
mSink->AddBezier(
D2D1::BezierSegment(D2DPoint(aCP1), D2DPoint(aCP2), D2DPoint(aCP3)));
mCurrentPoint = aCP3;
mFigureEmpty = false;
}
void PathBuilderD2D::QuadraticBezierTo(const Point& aCP1, const Point& aCP2) {
EnsureActive(aCP1);
mSink->AddQuadraticBezier(
D2D1::QuadraticBezierSegment(D2DPoint(aCP1), D2DPoint(aCP2)));
mCurrentPoint = aCP2;
mFigureEmpty = false;
}
void PathBuilderD2D::Close() {
if (mFigureActive) {
mSink->EndFigure(D2D1_FIGURE_END_CLOSED);
mFigureActive = false;
EnsureActive(mBeginPoint);
}
}
void PathBuilderD2D::Arc(const Point& aOrigin, Float aRadius, Float aStartAngle,
Float aEndAngle, bool aAntiClockwise) {
MOZ_ASSERT(aRadius >= 0);
// We want aEndAngle to come numerically after aStartAngle when taking into
// account the sweep direction so that our calculation of the arcSize below
// (large or small) works.
Float sweepDirection = aAntiClockwise ? -1.0f : 1.0f;
Float arcSweepLeft = (aEndAngle - aStartAngle) * sweepDirection;
if (arcSweepLeft < 0) {
// This calculation moves aStartAngle by a multiple of 2*Pi so that it is
// the closest it can be to aEndAngle and still be numerically before
// aEndAngle when taking into account sweepDirection.
arcSweepLeft = Float(2.0f * M_PI) + fmodf(arcSweepLeft, Float(2.0f * M_PI));
aStartAngle = aEndAngle - arcSweepLeft * sweepDirection;
}
// XXX - Workaround for now, D2D does not appear to do the desired thing when
// the angle sweeps a complete circle.
bool fullCircle = false;
if (aEndAngle - aStartAngle >= 2 * M_PI) {
fullCircle = true;
aEndAngle = Float(aStartAngle + M_PI * 1.9999);
} else if (aStartAngle - aEndAngle >= 2 * M_PI) {
fullCircle = true;
aStartAngle = Float(aEndAngle + M_PI * 1.9999);
}
Point startPoint;
startPoint.x = aOrigin.x + aRadius * cos(aStartAngle);
startPoint.y = aOrigin.y + aRadius * sin(aStartAngle);
if (!mFigureActive) {
EnsureActive(startPoint);
} else {
mSink->AddLine(D2DPoint(startPoint));
}
Point endPoint;
endPoint.x = aOrigin.x + aRadius * cosf(aEndAngle);
endPoint.y = aOrigin.y + aRadius * sinf(aEndAngle);
D2D1_ARC_SIZE arcSize = D2D1_ARC_SIZE_SMALL;
D2D1_SWEEP_DIRECTION direction = aAntiClockwise
? D2D1_SWEEP_DIRECTION_COUNTER_CLOCKWISE
: D2D1_SWEEP_DIRECTION_CLOCKWISE;
// if startPoint and endPoint of our circle are too close there are D2D issues
// with drawing the circle as a single arc
const Float kEpsilon = 1e-5f;
if (!fullCircle || (std::abs(startPoint.x - endPoint.x) +
std::abs(startPoint.y - endPoint.y) >
kEpsilon)) {
if (aAntiClockwise) {
if (aStartAngle - aEndAngle > M_PI) {
arcSize = D2D1_ARC_SIZE_LARGE;
}
} else {
if (aEndAngle - aStartAngle > M_PI) {
arcSize = D2D1_ARC_SIZE_LARGE;
}
}
mSink->AddArc(D2D1::ArcSegment(D2DPoint(endPoint),
D2D1::SizeF(aRadius, aRadius), 0.0f,
direction, arcSize));
} else {
// our first workaround attempt didn't work, so instead draw the circle as
// two half-circles
Float midAngle = aEndAngle > aStartAngle ? Float(aStartAngle + M_PI)
: Float(aEndAngle + M_PI);
Point midPoint;
midPoint.x = aOrigin.x + aRadius * cosf(midAngle);
midPoint.y = aOrigin.y + aRadius * sinf(midAngle);
mSink->AddArc(D2D1::ArcSegment(D2DPoint(midPoint),
D2D1::SizeF(aRadius, aRadius), 0.0f,
direction, arcSize));
// if the adjusted endPoint computed above is used here and endPoint !=
// startPoint then this half of the circle won't render...
mSink->AddArc(D2D1::ArcSegment(D2DPoint(startPoint),
D2D1::SizeF(aRadius, aRadius), 0.0f,
direction, arcSize));
}
mCurrentPoint = endPoint;
mFigureEmpty = false;
}
void PathBuilderD2D::EnsureActive(const Point& aPoint) {
if (!mFigureActive) {
mSink->BeginFigure(D2DPoint(aPoint), D2D1_FIGURE_BEGIN_FILLED);
mBeginPoint = aPoint;
mFigureActive = true;
}
}
already_AddRefed<Path> PathBuilderD2D::Finish() {
if (mFigureActive) {
mSink->EndFigure(D2D1_FIGURE_END_OPEN);
}
HRESULT hr = mSink->Close();
if (FAILED(hr)) {
gfxCriticalNote << "Failed to close PathSink. Code: " << hexa(hr);
return nullptr;
}
return MakeAndAddRef<PathD2D>(mGeometry, mFigureActive, mFigureEmpty,
mCurrentPoint, mFillRule, mBackendType);
}
already_AddRefed<PathBuilder> PathD2D::CopyToBuilder(FillRule aFillRule) const {
return TransformedCopyToBuilder(Matrix(), aFillRule);
}
already_AddRefed<PathBuilder> PathD2D::TransformedCopyToBuilder(
const Matrix& aTransform, FillRule aFillRule) const {
RefPtr<ID2D1PathGeometry> path;
HRESULT hr =
DrawTargetD2D1::factory()->CreatePathGeometry(getter_AddRefs(path));
if (FAILED(hr)) {
gfxWarning() << "Failed to create PathGeometry. Code: " << hexa(hr);
return nullptr;
}
RefPtr<ID2D1GeometrySink> sink;
hr = path->Open(getter_AddRefs(sink));
if (FAILED(hr)) {
gfxWarning() << "Failed to open Geometry for writing. Code: " << hexa(hr);
return nullptr;
}
if (aFillRule == FillRule::FILL_WINDING) {
sink->SetFillMode(D2D1_FILL_MODE_WINDING);
}
if (mEndedActive) {
OpeningGeometrySink wrapSink(sink);
hr = mGeometry->Simplify(
D2D1_GEOMETRY_SIMPLIFICATION_OPTION_CUBICS_AND_LINES,
D2DMatrix(aTransform), &wrapSink);
} else {
hr = mGeometry->Simplify(
D2D1_GEOMETRY_SIMPLIFICATION_OPTION_CUBICS_AND_LINES,
D2DMatrix(aTransform), sink);
}
if (FAILED(hr)) {
gfxWarning() << "Failed to simplify PathGeometry to tranformed copy. Code: "
<< hexa(hr) << " Active: " << mEndedActive;
return nullptr;
}
RefPtr<PathBuilderD2D> pathBuilder =
new PathBuilderD2D(sink, path, aFillRule, mBackendType);
pathBuilder->mCurrentPoint = aTransform.TransformPoint(mEndPoint);
if (mEndedActive) {
pathBuilder->mFigureActive = true;
}
return pathBuilder.forget();
}
void PathD2D::StreamToSink(PathSink* aSink) const {
HRESULT hr;
StreamingGeometrySink sink(aSink);
hr = mGeometry->Simplify(D2D1_GEOMETRY_SIMPLIFICATION_OPTION_CUBICS_AND_LINES,
D2D1::IdentityMatrix(), &sink);
if (FAILED(hr)) {
gfxWarning() << "Failed to stream D2D path to sink. Code: " << hexa(hr);
return;
}
}
bool PathD2D::ContainsPoint(const Point& aPoint,
const Matrix& aTransform) const {
if (!aTransform.Determinant()) {
// If the transform is not invertible, then don't consider point inside.
return false;
}
BOOL result;
HRESULT hr = mGeometry->FillContainsPoint(
D2DPoint(aPoint), D2DMatrix(aTransform), 0.001f, &result);
if (FAILED(hr)) {
// Log
return false;
}
return !!result;
}
bool PathD2D::StrokeContainsPoint(const StrokeOptions& aStrokeOptions,
const Point& aPoint,
const Matrix& aTransform) const {
if (!aTransform.Determinant()) {
// If the transform is not invertible, then don't consider point inside.
return false;
}
BOOL result;
RefPtr<ID2D1StrokeStyle> strokeStyle =
CreateStrokeStyleForOptions(aStrokeOptions);
HRESULT hr = mGeometry->StrokeContainsPoint(
D2DPoint(aPoint), aStrokeOptions.mLineWidth, strokeStyle,
D2DMatrix(aTransform), &result);
if (FAILED(hr)) {
// Log
return false;
}
return !!result;
}
Rect PathD2D::GetBounds(const Matrix& aTransform) const {
D2D1_RECT_F d2dBounds;
HRESULT hr = mGeometry->GetBounds(D2DMatrix(aTransform), &d2dBounds);
Rect bounds = ToRect(d2dBounds);
if (FAILED(hr) || !bounds.IsFinite()) {
gfxWarning() << "Failed to get stroked bounds for path. Code: " << hexa(hr);
return Rect();
}
return bounds;
}
Rect PathD2D::GetStrokedBounds(const StrokeOptions& aStrokeOptions,
const Matrix& aTransform) const {
D2D1_RECT_F d2dBounds;
RefPtr<ID2D1StrokeStyle> strokeStyle =
CreateStrokeStyleForOptions(aStrokeOptions);
HRESULT hr =
mGeometry->GetWidenedBounds(aStrokeOptions.mLineWidth, strokeStyle,
D2DMatrix(aTransform), &d2dBounds);
Rect bounds = ToRect(d2dBounds);
if (FAILED(hr) || !bounds.IsFinite()) {
gfxWarning() << "Failed to get stroked bounds for path. Code: " << hexa(hr);
return Rect();
}
return bounds;
}
} // namespace gfx
} // namespace mozilla